2026-07-16 08:19:17 +00:00
|
|
|
import Container from '@/components/shared/Container'
|
|
|
|
|
import { Loading } from '@/components/shared'
|
|
|
|
|
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
|
|
|
|
import { Dialog, Notification, toast } from '@/components/ui'
|
|
|
|
|
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
|
|
|
|
|
import {
|
|
|
|
|
DbTypeEnum,
|
|
|
|
|
EditingFormItemDto,
|
|
|
|
|
GridDto,
|
|
|
|
|
PlatformEditorTypes,
|
|
|
|
|
SubFormTabTypeEnum,
|
|
|
|
|
} from '@/proxy/form/models'
|
|
|
|
|
import { getList } from '@/services/form.service'
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
2026-07-16 14:01:18 +00:00
|
|
|
import { useStoreActions, useStoreState } from '@/store'
|
2026-07-16 08:19:17 +00:00
|
|
|
import DxCardView, {
|
|
|
|
|
CardViewRef,
|
|
|
|
|
CardViewTypes,
|
|
|
|
|
CardHeader,
|
|
|
|
|
ColumnChooser,
|
|
|
|
|
Editing,
|
|
|
|
|
FilterPanel,
|
|
|
|
|
HeaderFilter,
|
|
|
|
|
LoadPanel,
|
|
|
|
|
Pager,
|
|
|
|
|
Paging,
|
|
|
|
|
SearchPanel,
|
|
|
|
|
Selection,
|
|
|
|
|
Sorting,
|
|
|
|
|
Toolbar,
|
|
|
|
|
ToolbarItem,
|
|
|
|
|
} from 'devextreme-react/card-view'
|
|
|
|
|
import CustomStore from 'devextreme/data/custom_store'
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
|
|
import { createRoot } from 'react-dom/client'
|
|
|
|
|
import { Helmet } from 'react-helmet'
|
|
|
|
|
import { NotePanel } from '../form/notes/NotePanel'
|
|
|
|
|
import SubForms from '../form/SubForms'
|
|
|
|
|
import GridFilterDialogs from './GridFilterDialogs'
|
|
|
|
|
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
|
|
|
|
|
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
|
|
|
|
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
|
|
|
|
|
import { extractSearchParamsFields, GridExtraFilterState, safeJsonParse } from './Utils'
|
|
|
|
|
import { useFilters } from './useFilters'
|
|
|
|
|
import { useListFormColumns } from './useListFormColumns'
|
|
|
|
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
|
|
|
|
import { orderListToolbarItems } from './toolbarOrder'
|
|
|
|
|
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
|
|
|
|
|
import { layoutTypes } from '../admin/listForm/edit/types'
|
|
|
|
|
import { RowMode } from '../form/types'
|
|
|
|
|
|
|
|
|
|
interface CardViewProps {
|
|
|
|
|
listFormCode: string
|
|
|
|
|
searchParams?: URLSearchParams
|
|
|
|
|
isSubForm?: boolean
|
|
|
|
|
level?: number
|
|
|
|
|
refreshData?: () => Promise<void>
|
|
|
|
|
gridDto?: GridDto
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 14:01:18 +00:00
|
|
|
const cardColumnCountOptions = [1, 2, 3, 4, 5] as const
|
|
|
|
|
const defaultCardColumnCount = 3
|
|
|
|
|
|
2026-07-16 08:19:17 +00:00
|
|
|
const flattenColumns = (columns: any[] = []): any[] =>
|
|
|
|
|
columns.flatMap((column) => (column?.columns?.length ? flattenColumns(column.columns) : [column]))
|
|
|
|
|
|
|
|
|
|
const isTouchLikeDevice = () =>
|
|
|
|
|
typeof window !== 'undefined' &&
|
|
|
|
|
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
|
|
|
|
|
|
|
|
|
|
const isMobileViewport = () =>
|
|
|
|
|
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
|
|
|
|
|
|
|
|
|
|
const getPopupHeight = (useMobilePopup: boolean, fullScreen: boolean) =>
|
|
|
|
|
useMobilePopup && fullScreen ? '100%' : 'auto'
|
|
|
|
|
|
|
|
|
|
const getPopupMaxHeight = (
|
|
|
|
|
useMobilePopup: boolean,
|
|
|
|
|
fullScreen: boolean,
|
|
|
|
|
configuredHeight?: number,
|
|
|
|
|
) => {
|
|
|
|
|
if (useMobilePopup && fullScreen) return '100%'
|
|
|
|
|
return configuredHeight && configuredHeight > 0 ? configuredHeight : '90vh'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toCardColumn = (
|
|
|
|
|
column: any,
|
|
|
|
|
lookupDisplayValuesRef: { current: Record<string, Map<string, string>> },
|
|
|
|
|
): CardViewTypes.ColumnProperties => {
|
|
|
|
|
const dataField = column.dataField
|
|
|
|
|
const originalCustomizeText =
|
|
|
|
|
typeof column.customizeText === 'function' ? column.customizeText : undefined
|
|
|
|
|
const isImageColumn =
|
|
|
|
|
typeof column.cellTemplate === 'function' &&
|
|
|
|
|
Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions')
|
|
|
|
|
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
Object.entries({
|
|
|
|
|
alignment: column.alignment,
|
|
|
|
|
allowEditing: column.allowEditing,
|
|
|
|
|
allowFiltering: column.allowFiltering,
|
|
|
|
|
allowHeaderFiltering: column.allowHeaderFiltering,
|
|
|
|
|
allowReordering: column.allowReordering,
|
|
|
|
|
allowSearch: column.allowSearch,
|
|
|
|
|
allowSorting: column.allowSorting,
|
|
|
|
|
calculateDisplayValue:
|
|
|
|
|
typeof column.calculateDisplayValue === 'function'
|
|
|
|
|
? column.calculateDisplayValue
|
|
|
|
|
: undefined,
|
|
|
|
|
calculateFieldValue:
|
|
|
|
|
typeof column.calculateCellValue === 'function' ? column.calculateCellValue : undefined,
|
|
|
|
|
calculateFilterExpression:
|
|
|
|
|
typeof column.calculateFilterExpression === 'function'
|
|
|
|
|
? column.calculateFilterExpression
|
|
|
|
|
: undefined,
|
|
|
|
|
calculateSortValue: column.calculateSortValue,
|
|
|
|
|
caption: column.caption,
|
|
|
|
|
customizeText: column.lookup
|
|
|
|
|
? (fieldInfo: any) =>
|
|
|
|
|
lookupDisplayValuesRef.current[dataField]?.get(String(fieldInfo.value)) ??
|
|
|
|
|
originalCustomizeText?.(fieldInfo) ??
|
|
|
|
|
fieldInfo.valueText
|
|
|
|
|
: originalCustomizeText,
|
|
|
|
|
dataField: column.dataField,
|
|
|
|
|
dataType: column.dataType,
|
|
|
|
|
editorOptions: column.editorOptions,
|
|
|
|
|
filterValue: column.filterValue,
|
|
|
|
|
fieldValueTemplate:
|
|
|
|
|
isImageColumn && typeof column.cellTemplate === 'function'
|
|
|
|
|
? (templateOptions: any) => {
|
|
|
|
|
const templateData = templateOptions?.model ?? templateOptions
|
|
|
|
|
const container = document.createElement('div')
|
|
|
|
|
column.cellTemplate(container, {
|
|
|
|
|
column,
|
|
|
|
|
data: templateData.field?.card?.data,
|
|
|
|
|
displayValue: templateData.field?.displayValue,
|
|
|
|
|
value: templateData.field?.value,
|
|
|
|
|
})
|
|
|
|
|
return container
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
format: column.format,
|
|
|
|
|
headerFilter: column.headerFilter,
|
|
|
|
|
lookup: column.lookup,
|
|
|
|
|
name: column.name,
|
|
|
|
|
showInColumnChooser: column.showInColumnChooser,
|
|
|
|
|
sortIndex: column.sortIndex,
|
|
|
|
|
sortOrder: column.sortOrder,
|
|
|
|
|
validationRules: column.validationRules,
|
|
|
|
|
visible: column.visible,
|
|
|
|
|
visibleIndex: column.visibleIndex,
|
|
|
|
|
}).filter(([, value]) => value !== undefined),
|
|
|
|
|
) as CardViewTypes.ColumnProperties
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getValueByField = (data: Record<string, any>, field?: string | null) => {
|
|
|
|
|
if (!field) return undefined
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(data, field)) return data[field]
|
|
|
|
|
const key = Object.keys(data).find((item) => item.toLowerCase() === field.toLowerCase())
|
|
|
|
|
return key ? data[key] : undefined
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 14:01:18 +00:00
|
|
|
const getEditingFieldName = (dataField?: string | null) =>
|
|
|
|
|
String(dataField ?? '')
|
|
|
|
|
.split(':')[0]
|
|
|
|
|
.trim()
|
|
|
|
|
|
2026-07-16 08:19:17 +00:00
|
|
|
const lookupDisplayValuesEqual = (
|
|
|
|
|
left: Record<string, Map<string, string>>,
|
|
|
|
|
right: Record<string, Map<string, string>>,
|
|
|
|
|
) => {
|
|
|
|
|
const fields = Object.keys(left)
|
|
|
|
|
if (fields.length !== Object.keys(right).length) return false
|
|
|
|
|
return fields.every((field) => {
|
|
|
|
|
const leftValues = left[field]
|
|
|
|
|
const rightValues = right[field]
|
|
|
|
|
return (
|
|
|
|
|
rightValues?.size === leftValues.size &&
|
|
|
|
|
[...leftValues].every(([key, value]) => rightValues.get(key) === value)
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isGridDtoForList = (gridDto: GridDto | undefined, listFormCode: string) =>
|
|
|
|
|
gridDto?.gridOptions.listFormCode?.toLowerCase() === listFormCode.toLowerCase()
|
|
|
|
|
|
|
|
|
|
const getInitialExtraFilters = (gridDto?: GridDto): GridExtraFilterState[] =>
|
|
|
|
|
(gridDto?.gridOptions.extraFilterDto ?? []).map((filter) => ({
|
|
|
|
|
fieldName: filter.fieldName,
|
|
|
|
|
operator: filter.operator,
|
|
|
|
|
controlType: filter.controlType,
|
|
|
|
|
value: filter.defaultValue ?? '',
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const loadCascadeLookupDisplayValues = async (
|
|
|
|
|
columns: any[],
|
|
|
|
|
rows: Record<string, any>[],
|
|
|
|
|
knownValues: Record<string, Map<string, string>> = {},
|
|
|
|
|
) => {
|
|
|
|
|
const entries = await Promise.all(
|
|
|
|
|
columns
|
|
|
|
|
.filter((column) => column.colData?.lookupDto?.cascadeParentFields)
|
|
|
|
|
.map(async (column) => {
|
|
|
|
|
const lookup = column.colData.lookupDto
|
2026-07-16 14:01:18 +00:00
|
|
|
const parentFields: string[] = String(lookup.cascadeParentFields)
|
2026-07-16 08:19:17 +00:00
|
|
|
.split(',')
|
2026-07-16 14:01:18 +00:00
|
|
|
.map((field) => field.trim())
|
2026-07-16 08:19:17 +00:00
|
|
|
.filter(Boolean)
|
2026-07-16 14:01:18 +00:00
|
|
|
const parentValueLists: string[][] = parentFields.map((field) => {
|
2026-07-16 08:19:17 +00:00
|
|
|
const values = Object.entries(knownValues).find(
|
|
|
|
|
([key]) => key.toLowerCase() === field.toLowerCase(),
|
|
|
|
|
)?.[1]
|
|
|
|
|
return values ? [...values.keys()] : []
|
|
|
|
|
})
|
|
|
|
|
const preloadContexts = parentValueLists.every((values) => values.length > 0)
|
|
|
|
|
? parentValueLists.reduce<Record<string, any>[]>(
|
|
|
|
|
(contexts, values, index) =>
|
|
|
|
|
contexts
|
|
|
|
|
.flatMap((context) =>
|
|
|
|
|
values.map((value) => ({ ...context, [parentFields[index]]: value })),
|
|
|
|
|
)
|
|
|
|
|
.slice(0, 100),
|
|
|
|
|
[{}],
|
|
|
|
|
)
|
|
|
|
|
: []
|
|
|
|
|
const sourceRows = rows.length > 0 ? rows : preloadContexts
|
|
|
|
|
const contexts = [
|
|
|
|
|
...new Map(
|
|
|
|
|
sourceRows.map((row) => [
|
|
|
|
|
JSON.stringify(parentFields.map((field: string) => getValueByField(row, field))),
|
|
|
|
|
row,
|
|
|
|
|
]),
|
|
|
|
|
).values(),
|
|
|
|
|
]
|
|
|
|
|
const loadedItems = await Promise.all(
|
|
|
|
|
contexts.map(async (data) => {
|
|
|
|
|
const source = column.lookup?.dataSource?.({ data })
|
|
|
|
|
const store =
|
|
|
|
|
source instanceof CustomStore
|
|
|
|
|
? source
|
|
|
|
|
: (source as { store?: CustomStore<any, any> } | undefined)?.store
|
|
|
|
|
return ((await store?.load?.()) ?? []) as Record<string, any>[]
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
const values = new Map<string, string>()
|
|
|
|
|
loadedItems.flat().forEach((item) => {
|
|
|
|
|
const value = getValueByField(item, lookup.valueExpr)
|
|
|
|
|
const text = getValueByField(item, lookup.displayExpr)
|
|
|
|
|
if (value !== undefined && value !== null && text !== undefined && text !== null) {
|
|
|
|
|
values.set(String(value), String(text))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return [column.dataField, values] as const
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return Object.fromEntries(entries) as Record<string, Map<string, string>>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getCardLoadKey = (options: any) =>
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
filter: options.filter,
|
|
|
|
|
group: options.group,
|
|
|
|
|
groupSummary: options.groupSummary,
|
|
|
|
|
requireGroupCount: options.requireGroupCount,
|
|
|
|
|
requireTotalCount: options.requireTotalCount,
|
|
|
|
|
searchExpr: options.searchExpr,
|
|
|
|
|
searchOperation: options.searchOperation,
|
|
|
|
|
searchValue: options.searchValue,
|
|
|
|
|
skip: options.skip,
|
|
|
|
|
sort: options.sort,
|
|
|
|
|
take: options.take,
|
|
|
|
|
totalSummary: options.totalSummary,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getCardCountKey = (options: any) =>
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
filter: options.filter,
|
|
|
|
|
group: options.group,
|
|
|
|
|
searchExpr: options.searchExpr,
|
|
|
|
|
searchOperation: options.searchOperation,
|
|
|
|
|
searchValue: options.searchValue,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const deduplicateCardStoreLoads = (store: CustomStore<any, any>) => {
|
|
|
|
|
const cardStore = store as any
|
|
|
|
|
const originalLoad = cardStore._loadFunc?.bind(cardStore)
|
|
|
|
|
if (!originalLoad) return store
|
|
|
|
|
|
|
|
|
|
const cachedLoads = new Map<string, any>()
|
|
|
|
|
const pendingLoads = new Map<string, Promise<any>>()
|
|
|
|
|
const pendingTotalCounts = new Map<string, Promise<number | undefined>>()
|
|
|
|
|
const totalCounts = new Map<string, number>()
|
|
|
|
|
const invalidateSelectCache = () => {
|
|
|
|
|
cachedLoads.clear()
|
|
|
|
|
pendingLoads.clear()
|
|
|
|
|
pendingTotalCounts.clear()
|
|
|
|
|
totalCounts.clear()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cardStore._loadFunc = (options: any) => {
|
|
|
|
|
const loadKey = getCardLoadKey(options)
|
|
|
|
|
if (cachedLoads.has(loadKey)) return Promise.resolve(cachedLoads.get(loadKey))
|
|
|
|
|
const pending = pendingLoads.get(loadKey)
|
|
|
|
|
if (pending) return pending
|
|
|
|
|
|
|
|
|
|
const originalRequest = originalLoad(options)
|
|
|
|
|
const effectiveLoadKey = getCardLoadKey(options)
|
|
|
|
|
const request = Promise.resolve(originalRequest)
|
|
|
|
|
.then((result) => {
|
|
|
|
|
cachedLoads.set(loadKey, result)
|
|
|
|
|
cachedLoads.set(effectiveLoadKey, result)
|
|
|
|
|
if (typeof result?.totalCount === 'number') {
|
|
|
|
|
totalCounts.set(getCardCountKey(options), result.totalCount)
|
|
|
|
|
}
|
|
|
|
|
while (cachedLoads.size > 20) {
|
|
|
|
|
const oldestKey = cachedLoads.keys().next().value
|
|
|
|
|
if (oldestKey === undefined) break
|
|
|
|
|
cachedLoads.delete(oldestKey)
|
|
|
|
|
totalCounts.delete(oldestKey)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
pendingLoads.delete(loadKey)
|
|
|
|
|
pendingLoads.delete(effectiveLoadKey)
|
|
|
|
|
})
|
|
|
|
|
pendingLoads.set(loadKey, request)
|
|
|
|
|
pendingLoads.set(effectiveLoadKey, request)
|
|
|
|
|
const countKey = getCardCountKey(options)
|
|
|
|
|
const countRequest = request.then((result) =>
|
|
|
|
|
typeof result?.totalCount === 'number' ? result.totalCount : undefined,
|
|
|
|
|
)
|
|
|
|
|
pendingTotalCounts.set(countKey, countRequest)
|
|
|
|
|
countRequest.then(
|
|
|
|
|
() => pendingTotalCounts.delete(countKey),
|
|
|
|
|
() => pendingTotalCounts.delete(countKey),
|
|
|
|
|
)
|
|
|
|
|
return request
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originalTotalCount = cardStore._totalCountFunc?.bind(cardStore)
|
|
|
|
|
if (originalTotalCount) {
|
|
|
|
|
cardStore._totalCountFunc = (options: any) => {
|
|
|
|
|
const countKey = getCardCountKey(options)
|
|
|
|
|
const cachedCount = totalCounts.get(countKey)
|
|
|
|
|
if (cachedCount !== undefined) return Promise.resolve(cachedCount)
|
|
|
|
|
const pendingCount = pendingTotalCounts.get(countKey)
|
|
|
|
|
return pendingCount
|
2026-07-16 14:01:18 +00:00
|
|
|
? pendingCount.then((count) => (count !== undefined ? count : originalTotalCount(options)))
|
2026-07-16 08:19:17 +00:00
|
|
|
: originalTotalCount(options)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
;['_insertFunc', '_updateFunc', '_removeFunc'].forEach((functionName) => {
|
|
|
|
|
const originalMutation = cardStore[functionName]?.bind(cardStore)
|
|
|
|
|
if (!originalMutation) return
|
|
|
|
|
cardStore[functionName] = (...args: any[]) => {
|
|
|
|
|
invalidateSelectCache()
|
|
|
|
|
return originalMutation(...args)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
cardStore.invalidateSelectCache = invalidateSelectCache
|
|
|
|
|
return store
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CardView = (props: CardViewProps) => {
|
|
|
|
|
const {
|
|
|
|
|
listFormCode,
|
|
|
|
|
searchParams,
|
|
|
|
|
isSubForm,
|
|
|
|
|
level,
|
|
|
|
|
refreshData: refreshParentData,
|
|
|
|
|
gridDto: externalGridDto,
|
|
|
|
|
} = props
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
const { checkPermission } = usePermission()
|
|
|
|
|
const currentUser = useStoreState((state) => state.auth.user)
|
2026-07-16 14:01:18 +00:00
|
|
|
const savedListState = useStoreState((state) =>
|
|
|
|
|
state.admin.lists.states.find((item) => item.listFormCode === listFormCode),
|
|
|
|
|
)
|
|
|
|
|
const cardColumnCount = savedListState?.cardColumnCount ?? defaultCardColumnCount
|
|
|
|
|
const setListState = useStoreActions((actions) => actions.admin.lists.setStates)
|
|
|
|
|
const currentCulture = useStoreState(
|
|
|
|
|
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
|
|
|
|
)
|
|
|
|
|
const localizationTexts = useStoreState((state) => state.abpConfig.texts)
|
2026-07-16 08:19:17 +00:00
|
|
|
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
|
|
|
|
const cardViewRef = useRef<CardViewRef>()
|
|
|
|
|
const searchParamsRef = useRef(searchParams)
|
|
|
|
|
const defaultSearchFilterRef = useRef(searchParams?.get('filter') ?? null)
|
|
|
|
|
const dataSourceSearchParamsRef = useRef<URLSearchParams>()
|
|
|
|
|
const extraFilterMountedRef = useRef(false)
|
|
|
|
|
const [gridDtoState, setGridDtoState] = useState<{
|
|
|
|
|
listFormCode: string
|
|
|
|
|
value?: GridDto
|
|
|
|
|
}>(() => ({
|
|
|
|
|
listFormCode,
|
|
|
|
|
value: isGridDtoForList(externalGridDto, listFormCode) ? externalGridDto : undefined,
|
|
|
|
|
}))
|
|
|
|
|
const gridDto = gridDtoState.listFormCode === listFormCode ? gridDtoState.value : undefined
|
|
|
|
|
const [mode, setMode] = useState<RowMode>('view')
|
|
|
|
|
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
|
|
|
|
const [formData, setFormData] = useState<Record<string, any>>()
|
|
|
|
|
const formDataRef = useRef<Record<string, any>>()
|
|
|
|
|
formDataRef.current = formData
|
|
|
|
|
const lookupDisplayValuesRef = useRef<Record<string, Map<string, string>>>({})
|
|
|
|
|
const [lookupReadyState, setLookupReadyState] = useState({ listFormCode, ready: false })
|
|
|
|
|
const [notePanelTarget, setNotePanelTarget] = useState<{
|
|
|
|
|
entityName: string
|
|
|
|
|
entityId: string
|
|
|
|
|
} | null>(null)
|
|
|
|
|
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>(() =>
|
|
|
|
|
getInitialExtraFilters(
|
|
|
|
|
isGridDtoForList(externalGridDto, listFormCode) ? externalGridDto : undefined,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const mergedExtraFilter = useMemo(() => {
|
|
|
|
|
const base = safeJsonParse(defaultSearchFilterRef.current, null, 'Search filter parse error:')
|
|
|
|
|
const triplets = [
|
|
|
|
|
...extractSearchParamsFields(base),
|
|
|
|
|
...extraFilters
|
|
|
|
|
.filter(
|
|
|
|
|
(filter) => filter.value !== undefined && filter.value !== null && filter.value !== '',
|
|
|
|
|
)
|
|
|
|
|
.map(
|
|
|
|
|
(filter) =>
|
|
|
|
|
[filter.fieldName, filter.operator, filter.value] as [string, string, unknown],
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
const unique = triplets.reduce(
|
|
|
|
|
(result, current) => {
|
|
|
|
|
const index = result.findIndex((item) => item[0] === current[0] && item[1] === current[1])
|
|
|
|
|
if (index >= 0) result[index] = current
|
|
|
|
|
else result.push(current)
|
|
|
|
|
return result
|
|
|
|
|
},
|
|
|
|
|
[] as [string, string, unknown][],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return unique.reduce<any>((result, filter) => (result ? [result, 'and', filter] : filter), null)
|
|
|
|
|
}, [extraFilters])
|
|
|
|
|
|
|
|
|
|
if (!dataSourceSearchParamsRef.current) {
|
|
|
|
|
dataSourceSearchParamsRef.current = new URLSearchParams(searchParams?.toString())
|
|
|
|
|
if (mergedExtraFilter) {
|
|
|
|
|
dataSourceSearchParamsRef.current.set('filter', JSON.stringify(mergedExtraFilter))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
searchParamsRef.current = searchParams
|
|
|
|
|
defaultSearchFilterRef.current = searchParams?.get('filter') ?? null
|
|
|
|
|
dataSourceSearchParamsRef.current = undefined
|
|
|
|
|
extraFilterMountedRef.current = false
|
|
|
|
|
setExtraFilters([])
|
|
|
|
|
lookupDisplayValuesRef.current = {}
|
|
|
|
|
setLookupReadyState({ listFormCode, ready: false })
|
|
|
|
|
setFormData(undefined)
|
|
|
|
|
setMode('view')
|
|
|
|
|
}, [listFormCode, searchParams])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isGridDtoForList(externalGridDto, listFormCode)) {
|
|
|
|
|
setGridDtoState({ listFormCode, value: externalGridDto })
|
|
|
|
|
setExtraFilters(getInitialExtraFilters(externalGridDto))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let active = true
|
|
|
|
|
getList({ listFormCode }).then((response: any) => {
|
|
|
|
|
if (active) {
|
|
|
|
|
setGridDtoState({ listFormCode, value: response.data })
|
|
|
|
|
setExtraFilters(getInitialExtraFilters(response.data))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return () => {
|
|
|
|
|
active = false
|
|
|
|
|
}
|
|
|
|
|
}, [externalGridDto, listFormCode])
|
|
|
|
|
|
|
|
|
|
const refreshData = useCallback(() => {
|
|
|
|
|
const cardDataSource = cardViewRef.current?.instance().getDataSource()
|
|
|
|
|
;(cardDataSource?.store() as any)?.invalidateSelectCache?.()
|
|
|
|
|
const reload = cardDataSource?.reload()
|
|
|
|
|
Promise.resolve(reload).finally(() => refreshParentData?.())
|
|
|
|
|
}, [refreshParentData])
|
2026-07-16 14:01:18 +00:00
|
|
|
const setCardColumnCount = useCallback(
|
|
|
|
|
(cardColumnCount: number) => {
|
|
|
|
|
setListState({
|
|
|
|
|
listFormCode,
|
|
|
|
|
layout: savedListState?.layout ?? 'card',
|
|
|
|
|
cardColumnCount,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[listFormCode, savedListState?.layout, setListState],
|
|
|
|
|
)
|
2026-07-16 08:19:17 +00:00
|
|
|
const listFormColumnsRef = useRef({
|
|
|
|
|
current: {
|
|
|
|
|
instance: () => ({ refresh: refreshData }),
|
|
|
|
|
},
|
|
|
|
|
}).current
|
|
|
|
|
|
|
|
|
|
const openNotePanel = useCallback(
|
|
|
|
|
(rowData: Record<string, any>) => {
|
|
|
|
|
const entityId = getValueByField(rowData, gridDto?.gridOptions.keyFieldName)
|
|
|
|
|
if (entityId === undefined || entityId === null || entityId === '') return
|
|
|
|
|
|
|
|
|
|
setNotePanelTarget({
|
|
|
|
|
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
|
|
|
|
|
entityId: String(entityId),
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[gridDto, listFormCode],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
|
|
|
|
|
gridDto,
|
|
|
|
|
listFormCode,
|
|
|
|
|
isSubForm,
|
|
|
|
|
gridRef: listFormColumnsRef,
|
|
|
|
|
onShowNote: openNotePanel,
|
|
|
|
|
})
|
2026-07-16 14:01:18 +00:00
|
|
|
const localizationRevisionRef = useRef({
|
|
|
|
|
culture: currentCulture,
|
|
|
|
|
texts: localizationTexts,
|
|
|
|
|
value: 0,
|
|
|
|
|
})
|
|
|
|
|
if (
|
|
|
|
|
localizationRevisionRef.current.culture !== currentCulture ||
|
|
|
|
|
localizationRevisionRef.current.texts !== localizationTexts
|
|
|
|
|
) {
|
|
|
|
|
localizationRevisionRef.current = {
|
|
|
|
|
culture: currentCulture,
|
|
|
|
|
texts: localizationTexts,
|
|
|
|
|
value: localizationRevisionRef.current.value + 1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const localizationRevision = localizationRevisionRef.current.value
|
|
|
|
|
const listColumnsCacheRef = useRef<{
|
|
|
|
|
gridDto?: GridDto
|
|
|
|
|
localizationRevision?: number
|
|
|
|
|
columns: any[]
|
|
|
|
|
}>({ columns: [] })
|
|
|
|
|
if (
|
|
|
|
|
listColumnsCacheRef.current.gridDto !== gridDto ||
|
|
|
|
|
listColumnsCacheRef.current.localizationRevision !== localizationRevision
|
|
|
|
|
) {
|
2026-07-16 08:19:17 +00:00
|
|
|
listColumnsCacheRef.current = {
|
|
|
|
|
gridDto,
|
2026-07-16 14:01:18 +00:00
|
|
|
localizationRevision,
|
2026-07-16 08:19:17 +00:00
|
|
|
columns: flattenColumns(getBandedColumns()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const listColumns = listColumnsCacheRef.current.columns
|
|
|
|
|
const cardColumnsCacheRef = useRef<{
|
2026-07-16 14:01:18 +00:00
|
|
|
listColumns?: any[]
|
2026-07-16 08:19:17 +00:00
|
|
|
columns: CardViewTypes.ColumnProperties[]
|
|
|
|
|
}>({ columns: [] })
|
2026-07-16 14:01:18 +00:00
|
|
|
if (cardColumnsCacheRef.current.listColumns !== listColumns) {
|
2026-07-16 08:19:17 +00:00
|
|
|
cardColumnsCacheRef.current = {
|
2026-07-16 14:01:18 +00:00
|
|
|
listColumns,
|
2026-07-16 08:19:17 +00:00
|
|
|
columns: listColumns
|
|
|
|
|
.filter((column) => column?.type !== 'buttons' && column?.dataField)
|
|
|
|
|
.map((column) => toCardColumn(column, lookupDisplayValuesRef)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const cardColumns = cardColumnsCacheRef.current.columns
|
|
|
|
|
const cardColumnsRef = useRef(cardColumns)
|
|
|
|
|
cardColumnsRef.current = cardColumns
|
|
|
|
|
const commandButtons = listColumns.find((column) => column?.type === 'buttons')?.buttons ?? []
|
|
|
|
|
const commandButtonsRef = useRef(commandButtons)
|
|
|
|
|
commandButtonsRef.current = commandButtons
|
|
|
|
|
|
|
|
|
|
const renderImageUploadEditor = useCallback((templateData: any) => {
|
2026-07-16 14:01:18 +00:00
|
|
|
const configuredDataField =
|
2026-07-16 08:19:17 +00:00
|
|
|
templateData?.dataField ??
|
|
|
|
|
templateData?.item?.dataField ??
|
|
|
|
|
templateData?.editorOptions?.name ??
|
|
|
|
|
templateData?.name
|
2026-07-16 14:01:18 +00:00
|
|
|
const dataField = getEditingFieldName(configuredDataField)
|
2026-07-16 08:19:17 +00:00
|
|
|
const column = listColumnsCacheRef.current.columns.find(
|
2026-07-16 14:01:18 +00:00
|
|
|
(candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
|
2026-07-16 08:19:17 +00:00
|
|
|
)
|
|
|
|
|
const component = templateData?.component
|
|
|
|
|
const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
|
2026-07-16 14:01:18 +00:00
|
|
|
const editorValue = templateData?.editorOptions?.value
|
2026-07-16 08:19:17 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ImageUploadEditorComponent
|
|
|
|
|
{...templateData}
|
|
|
|
|
column={column}
|
2026-07-16 14:01:18 +00:00
|
|
|
value={
|
|
|
|
|
editorValue !== undefined ? editorValue : getValueByField(currentFormData, dataField)
|
|
|
|
|
}
|
2026-07-16 08:19:17 +00:00
|
|
|
setValue={(value: any) => {
|
|
|
|
|
if (!dataField) return
|
2026-07-16 14:01:18 +00:00
|
|
|
if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
|
|
|
|
|
templateData.editorOptions.onValueChanged({ value })
|
|
|
|
|
} else {
|
|
|
|
|
component?.updateData?.(dataField, value)
|
|
|
|
|
}
|
2026-07-16 08:19:17 +00:00
|
|
|
formDataRef.current = { ...currentFormData, [dataField]: value }
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const renderImageViewerEditor = useCallback(
|
|
|
|
|
(templateData: any) => (
|
|
|
|
|
<ImageViewerEditorComponent {...templateData} fallbackFormData={formDataRef.current} />
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const imageUploadFormTemplate = useCallback(
|
|
|
|
|
(templateOptions: any) => {
|
|
|
|
|
const container = document.createElement('div')
|
|
|
|
|
createRoot(container).render(
|
|
|
|
|
renderImageUploadEditor(templateOptions?.model ?? templateOptions),
|
|
|
|
|
)
|
|
|
|
|
return container
|
|
|
|
|
},
|
|
|
|
|
[renderImageUploadEditor],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const imageViewerFormTemplate = useCallback(
|
|
|
|
|
(templateOptions: any) => {
|
|
|
|
|
const container = document.createElement('div')
|
|
|
|
|
createRoot(container).render(
|
|
|
|
|
renderImageViewerEditor(templateOptions?.model ?? templateOptions),
|
|
|
|
|
)
|
|
|
|
|
return container
|
|
|
|
|
},
|
|
|
|
|
[renderImageViewerEditor],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const cascadeChildrenByParent = useMemo(() => {
|
|
|
|
|
const result = new Map<string, string[]>()
|
|
|
|
|
gridDto?.columnFormats.forEach((column) => {
|
|
|
|
|
if (!column.fieldName || !column.lookupDto?.cascadeParentFields) return
|
|
|
|
|
column.lookupDto.cascadeParentFields.split(',').forEach((parentField) => {
|
|
|
|
|
const key = parentField.trim().toLowerCase()
|
|
|
|
|
if (!key) return
|
|
|
|
|
result.set(key, [...(result.get(key) ?? []), column.fieldName!])
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
return result
|
|
|
|
|
}, [gridDto])
|
|
|
|
|
|
|
|
|
|
const onEditingFormFieldChanged = useCallback(
|
|
|
|
|
(event: any) => {
|
|
|
|
|
if (!event.dataField) return
|
|
|
|
|
const childFields = cascadeChildrenByParent.get(event.dataField.toLowerCase())
|
|
|
|
|
const currentData = event.component?.option?.('formData') ?? formDataRef.current ?? {}
|
|
|
|
|
const nextData = { ...currentData, [event.dataField]: event.value }
|
|
|
|
|
formDataRef.current = nextData
|
|
|
|
|
if (!childFields?.length) return
|
|
|
|
|
|
|
|
|
|
childFields.forEach((childField) => {
|
|
|
|
|
const column = listColumnsCacheRef.current.columns.find(
|
|
|
|
|
(candidate) => candidate.dataField?.toLowerCase() === childField.toLowerCase(),
|
|
|
|
|
)
|
|
|
|
|
const source = column?.lookup?.dataSource?.({ data: nextData })
|
|
|
|
|
const parentFields = column?.colData?.lookupDto?.cascadeParentFields
|
|
|
|
|
?.split(',')
|
|
|
|
|
.map((field: string) => field.trim())
|
|
|
|
|
.filter(Boolean)
|
2026-07-16 14:01:18 +00:00
|
|
|
const disabled = parentFields?.some((field: string) => !getValueByField(nextData, field))
|
2026-07-16 08:19:17 +00:00
|
|
|
const editor = event.component?.getEditor?.(childField)
|
|
|
|
|
|
|
|
|
|
editor?.option?.({ dataSource: source, disabled: Boolean(disabled), value: null })
|
|
|
|
|
event.component?.updateData?.(childField, null)
|
|
|
|
|
nextData[childField] = null
|
|
|
|
|
})
|
|
|
|
|
formDataRef.current = nextData
|
|
|
|
|
setFormData(nextData)
|
|
|
|
|
},
|
|
|
|
|
[cascadeChildrenByParent],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-16 14:01:18 +00:00
|
|
|
const applyLookupDisplayValues = useCallback((values: Record<string, Map<string, string>>) => {
|
|
|
|
|
if (lookupDisplayValuesEqual(lookupDisplayValuesRef.current, values)) return
|
|
|
|
|
lookupDisplayValuesRef.current = values
|
|
|
|
|
}, [])
|
2026-07-16 08:19:17 +00:00
|
|
|
|
|
|
|
|
const getLookupColumns = useCallback(
|
|
|
|
|
() =>
|
|
|
|
|
listColumnsCacheRef.current.columns.filter(
|
|
|
|
|
(column) => column?.type !== 'buttons' && column?.dataField,
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!gridDto) return
|
|
|
|
|
|
|
|
|
|
let active = true
|
|
|
|
|
const columns = getLookupColumns()
|
|
|
|
|
loadLookupDisplayValues(columns)
|
|
|
|
|
.then(async (values) => ({
|
|
|
|
|
...values,
|
|
|
|
|
...(await loadCascadeLookupDisplayValues(columns, [], values)),
|
|
|
|
|
}))
|
|
|
|
|
.then((values) => {
|
|
|
|
|
if (!active) return
|
|
|
|
|
applyLookupDisplayValues(values)
|
|
|
|
|
setLookupReadyState({ listFormCode, ready: true })
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error('CardView lookup preload error:', error)
|
|
|
|
|
if (active) setLookupReadyState({ listFormCode, ready: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
active = false
|
|
|
|
|
}
|
|
|
|
|
}, [applyLookupDisplayValues, getLookupColumns, gridDto, listFormCode, loadLookupDisplayValues])
|
|
|
|
|
|
|
|
|
|
const onContentReady = useCallback(() => {
|
|
|
|
|
const rows = (cardViewRef.current?.instance().getDataSource().items() ?? []) as Record<
|
|
|
|
|
string,
|
|
|
|
|
any
|
|
|
|
|
>[]
|
|
|
|
|
const columns = getLookupColumns()
|
|
|
|
|
Promise.all([
|
|
|
|
|
loadLookupDisplayValues(columns),
|
|
|
|
|
loadCascadeLookupDisplayValues(columns, rows),
|
2026-07-16 14:01:18 +00:00
|
|
|
]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues }))
|
2026-07-16 08:19:17 +00:00
|
|
|
}, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues])
|
|
|
|
|
|
|
|
|
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef })
|
|
|
|
|
const dataSource = useMemo<CustomStore<any, any> | undefined>(() => {
|
|
|
|
|
if (!gridDto) return undefined
|
|
|
|
|
return deduplicateCardStoreLoads(
|
|
|
|
|
createSelectDataSource(
|
|
|
|
|
gridDto.gridOptions,
|
|
|
|
|
listFormCode,
|
|
|
|
|
dataSourceSearchParamsRef.current,
|
|
|
|
|
layoutTypes.card,
|
|
|
|
|
cardColumnsRef.current as any,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}, [createSelectDataSource, gridDto, listFormCode])
|
|
|
|
|
|
|
|
|
|
const { filterToolbarData, ...filterData } = useFilters({
|
|
|
|
|
gridDto,
|
|
|
|
|
gridRef: cardViewRef,
|
|
|
|
|
listFormCode,
|
|
|
|
|
})
|
|
|
|
|
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
|
|
|
|
|
gridDto,
|
|
|
|
|
listFormCode,
|
|
|
|
|
getSelectedRowKeys: () => cardViewRef.current?.instance().getSelectedCardKeys() ?? [],
|
|
|
|
|
getSelectedRowsData: () => cardViewRef.current?.instance().getSelectedCardsData() ?? [],
|
|
|
|
|
refreshData,
|
|
|
|
|
getFilter: () => cardViewRef.current?.instance().getCombinedFilter(),
|
|
|
|
|
layout: layoutTypes.card,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const orderedToolbarData = useMemo(() => {
|
|
|
|
|
const unsupportedItems = new Set([
|
|
|
|
|
'applyFilterButton',
|
|
|
|
|
'exportButton',
|
|
|
|
|
'fitColumns',
|
|
|
|
|
'groupPanel',
|
|
|
|
|
'resetGridState',
|
|
|
|
|
'saveGridState',
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return orderListToolbarItems([...toolbarData, ...filterToolbarData])
|
|
|
|
|
.filter((item) => !unsupportedItems.has(String(item.name)))
|
|
|
|
|
.map((item) => {
|
|
|
|
|
if (item.name !== 'addRowButton') return item
|
|
|
|
|
return {
|
|
|
|
|
...item,
|
|
|
|
|
name: 'addCardButton',
|
|
|
|
|
options: item.options?.onClick
|
|
|
|
|
? item.options
|
|
|
|
|
: {
|
|
|
|
|
...item.options,
|
|
|
|
|
onClick: () => cardViewRef.current?.instance().addCard(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}, [filterToolbarData, toolbarData])
|
|
|
|
|
|
|
|
|
|
const mapEditingFormItem = useCallback(
|
|
|
|
|
(item: EditingFormItemDto) => {
|
|
|
|
|
const fieldName = item.dataField.split(':')[0]
|
|
|
|
|
const column = gridDto?.columnFormats.find(
|
|
|
|
|
(candidate) => candidate.fieldName?.toLowerCase() === fieldName.toLowerCase(),
|
|
|
|
|
)
|
|
|
|
|
if (!column) return undefined
|
|
|
|
|
if (
|
|
|
|
|
mode === 'new' &&
|
|
|
|
|
fieldName.toLowerCase() === gridDto?.gridOptions.keyFieldName?.toLowerCase()
|
|
|
|
|
) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isAllowed =
|
|
|
|
|
mode === 'new'
|
|
|
|
|
? column.canCreate && column.allowAdding
|
|
|
|
|
: mode === 'edit'
|
|
|
|
|
? column.canUpdate && column.allowEditing
|
|
|
|
|
: false
|
|
|
|
|
if (!isAllowed) return undefined
|
|
|
|
|
|
|
|
|
|
let editorOptions: Record<string, any> = {}
|
|
|
|
|
try {
|
|
|
|
|
editorOptions = item.editorOptions ? JSON.parse(item.editorOptions) : {}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('EditorOptions parse error:', item.dataField, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (column.sourceDbType === DbTypeEnum.Date) {
|
|
|
|
|
editorOptions = {
|
|
|
|
|
type: 'date',
|
|
|
|
|
dateSerializationFormat: 'yyyy-MM-dd',
|
|
|
|
|
displayFormat: 'shortDate',
|
|
|
|
|
...editorOptions,
|
|
|
|
|
}
|
|
|
|
|
} else if (
|
|
|
|
|
column.sourceDbType === DbTypeEnum.DateTime ||
|
|
|
|
|
column.sourceDbType === DbTypeEnum.DateTime2 ||
|
|
|
|
|
column.sourceDbType === DbTypeEnum.DateTimeOffset
|
|
|
|
|
) {
|
|
|
|
|
editorOptions = {
|
|
|
|
|
type: 'datetime',
|
|
|
|
|
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
|
|
|
|
|
displayFormat: 'shortDateShortTime',
|
|
|
|
|
...editorOptions,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (column.placeHolder) {
|
|
|
|
|
editorOptions.placeholder = translate('::' + column.placeHolder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cardColumn = cardColumnsRef.current.find(
|
|
|
|
|
(candidate) => candidate.dataField?.toLowerCase() === fieldName.toLowerCase(),
|
|
|
|
|
) as any
|
|
|
|
|
const lookup = cardColumn?.lookup
|
|
|
|
|
if (lookup) {
|
|
|
|
|
editorOptions = {
|
|
|
|
|
...editorOptions,
|
|
|
|
|
dataSource:
|
|
|
|
|
typeof lookup.dataSource === 'function'
|
|
|
|
|
? lookup.dataSource({ data: formData ?? {} })
|
|
|
|
|
: lookup.dataSource,
|
|
|
|
|
displayExpr: lookup.displayExpr,
|
|
|
|
|
valueExpr: lookup.valueExpr,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
itemType: 'simple',
|
2026-07-16 14:01:18 +00:00
|
|
|
dataField: fieldName,
|
|
|
|
|
name: fieldName,
|
2026-07-16 08:19:17 +00:00
|
|
|
colSpan: item.colSpan,
|
|
|
|
|
label: {
|
|
|
|
|
text: item.dataField.includes(':')
|
|
|
|
|
? item.dataField.split(':')[1]
|
|
|
|
|
: translate('::' + (column.captionName || column.fieldName)),
|
|
|
|
|
},
|
|
|
|
|
editorType:
|
|
|
|
|
item.editorType2 === PlatformEditorTypes.dxGridBox
|
|
|
|
|
? 'dxDropDownBox'
|
|
|
|
|
: item.editorType2 === PlatformEditorTypes.dxImageUpload ||
|
|
|
|
|
item.editorType2 === PlatformEditorTypes.dxImageViewer
|
|
|
|
|
? undefined
|
|
|
|
|
: item.editorType2,
|
|
|
|
|
editorOptions,
|
|
|
|
|
template:
|
|
|
|
|
item.editorType2 === PlatformEditorTypes.dxImageUpload
|
|
|
|
|
? imageUploadFormTemplate
|
|
|
|
|
: item.editorType2 === PlatformEditorTypes.dxImageViewer
|
|
|
|
|
? imageViewerFormTemplate
|
|
|
|
|
: undefined,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[formData, gridDto, imageUploadFormTemplate, imageViewerFormTemplate, mode, translate],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const editingFormItems = useMemo(() => {
|
|
|
|
|
if (!gridDto || mode === 'view') return []
|
|
|
|
|
|
|
|
|
|
const groups = [...(gridDto.gridOptions.editingFormDto ?? [])].sort(
|
|
|
|
|
(left, right) => left.order - right.order,
|
|
|
|
|
)
|
|
|
|
|
const tabbedGroups = groups.filter((group) => group.itemType === 'tabbed')
|
|
|
|
|
const result: any[] = []
|
|
|
|
|
|
|
|
|
|
groups.forEach((group) => {
|
|
|
|
|
const items = [...(group.items ?? [])]
|
|
|
|
|
.sort((left, right) => left.order - right.order)
|
|
|
|
|
.map(mapEditingFormItem)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
|
|
|
if (group.itemType !== 'tabbed') {
|
|
|
|
|
if (!items.length) return
|
|
|
|
|
result.push({
|
|
|
|
|
itemType: group.itemType || 'group',
|
|
|
|
|
caption: group.caption,
|
|
|
|
|
colCount: useMobileEditPopup ? 1 : group.colCount || 1,
|
|
|
|
|
colSpan: group.colSpan || 1,
|
|
|
|
|
items,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (group !== tabbedGroups[0]) return
|
|
|
|
|
const tabs = tabbedGroups
|
|
|
|
|
.map((tab) => ({
|
|
|
|
|
title: tab.caption,
|
|
|
|
|
colCount: useMobileEditPopup ? 1 : tab.colCount || 1,
|
|
|
|
|
items: [...(tab.items ?? [])]
|
|
|
|
|
.sort((left, right) => left.order - right.order)
|
|
|
|
|
.map(mapEditingFormItem)
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
}))
|
|
|
|
|
.filter((tab) => tab.items.length > 0)
|
|
|
|
|
if (tabs.length) result.push({ itemType: 'tabbed', tabs })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}, [gridDto, mapEditingFormItem, mode, useMobileEditPopup])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!extraFilterMountedRef.current) {
|
|
|
|
|
extraFilterMountedRef.current = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (mergedExtraFilter) {
|
|
|
|
|
const serializedFilter = JSON.stringify(mergedExtraFilter)
|
|
|
|
|
searchParamsRef.current?.set('filter', serializedFilter)
|
|
|
|
|
dataSourceSearchParamsRef.current?.set('filter', serializedFilter)
|
|
|
|
|
} else {
|
|
|
|
|
searchParamsRef.current?.delete('filter')
|
|
|
|
|
dataSourceSearchParamsRef.current?.delete('filter')
|
|
|
|
|
}
|
|
|
|
|
cardViewRef.current?.instance().getDataSource()?.reload()
|
|
|
|
|
}, [mergedExtraFilter])
|
|
|
|
|
|
2026-07-16 14:01:18 +00:00
|
|
|
const renderCardHeader = useCallback((templateOptions: any, suppliedContainer?: HTMLElement) => {
|
|
|
|
|
const templateData = templateOptions?.model ?? templateOptions
|
|
|
|
|
const card = templateData?.card ?? templateData
|
|
|
|
|
const row = { data: card?.data ?? {}, isEditing: false, key: card?.key }
|
|
|
|
|
const templateContainer = templateOptions?.container ?? suppliedContainer
|
|
|
|
|
const domContainer =
|
|
|
|
|
templateContainer instanceof HTMLElement ? templateContainer : templateContainer?.get?.(0)
|
|
|
|
|
domContainer?.replaceChildren()
|
|
|
|
|
const actionsElement = document.createElement('div')
|
|
|
|
|
actionsElement.className =
|
|
|
|
|
'flex min-h-10 w-full flex-wrap items-center justify-end gap-1 px-2 py-1'
|
|
|
|
|
|
|
|
|
|
commandButtonsRef.current.forEach((action: any, index: number) => {
|
|
|
|
|
let visible = action.visible ?? true
|
|
|
|
|
if (typeof visible === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
visible = visible({ row, card })
|
|
|
|
|
} catch {
|
|
|
|
|
visible = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!visible) return
|
|
|
|
|
|
|
|
|
|
const name = String(action.name ?? action.hint ?? `action-${index}`)
|
|
|
|
|
const button = document.createElement('button')
|
|
|
|
|
button.type = 'button'
|
|
|
|
|
button.className =
|
|
|
|
|
'rounded px-2 py-1 text-xs font-medium hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
|
|
|
button.textContent = action.text || action.hint || name
|
|
|
|
|
button.title = action.hint ?? action.text ?? name
|
|
|
|
|
button.addEventListener('click', (event) => {
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
if (name === 'edit') {
|
|
|
|
|
cardViewRef.current?.instance().editCard(card.index)
|
|
|
|
|
} else if (name === 'delete') {
|
|
|
|
|
cardViewRef.current?.instance().deleteCard(card.index)
|
|
|
|
|
} else {
|
|
|
|
|
action.onClick?.({
|
|
|
|
|
event,
|
|
|
|
|
row,
|
|
|
|
|
card,
|
|
|
|
|
component: cardViewRef.current?.instance(),
|
|
|
|
|
})
|
2026-07-16 08:19:17 +00:00
|
|
|
}
|
|
|
|
|
})
|
2026-07-16 14:01:18 +00:00
|
|
|
actionsElement.appendChild(button)
|
|
|
|
|
})
|
2026-07-16 08:19:17 +00:00
|
|
|
|
2026-07-16 14:01:18 +00:00
|
|
|
return actionsElement
|
|
|
|
|
}, [])
|
2026-07-16 08:19:17 +00:00
|
|
|
|
|
|
|
|
const editingOptions = gridDto?.gridOptions.editingOptionDto
|
|
|
|
|
const selectionOptions = gridDto?.gridOptions.selectionDto
|
|
|
|
|
|
|
|
|
|
const onSelectionChanged = useCallback(
|
|
|
|
|
(event: CardViewTypes.SelectionChangedEvent) => {
|
|
|
|
|
const selectedCardsData = event.selectedCardsData as Record<string, any>[]
|
|
|
|
|
setFormData(selectedCardsData[0])
|
|
|
|
|
|
|
|
|
|
const component = event.component
|
|
|
|
|
const toolbar = component.option('toolbar') as any
|
|
|
|
|
const deleteSelectedItemIndex = toolbar?.items?.findIndex(
|
|
|
|
|
(item: any) => item.name === 'deleteSelectedRecords',
|
|
|
|
|
)
|
|
|
|
|
if (deleteSelectedItemIndex >= 0) {
|
|
|
|
|
const optionPath = `toolbar.items[${deleteSelectedItemIndex}].options.visible`
|
|
|
|
|
const nextVisible = selectedCardsData.length > 1
|
|
|
|
|
if (component.option(optionPath) !== nextVisible) {
|
|
|
|
|
component.option(optionPath, nextVisible)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateWorkflowApprovalToolbarItems(
|
|
|
|
|
component,
|
|
|
|
|
gridDto?.gridOptions.workflowDto,
|
|
|
|
|
selectedCardsData,
|
|
|
|
|
currentUser,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
[currentUser, gridDto],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Container className={DX_CLASSNAMES}>
|
|
|
|
|
{!isSubForm && (
|
|
|
|
|
<Helmet
|
|
|
|
|
titleTemplate={`%s | ${APP_NAME}`}
|
|
|
|
|
title={translate('::' + gridDto?.gridOptions.title)}
|
|
|
|
|
defaultTitle={APP_NAME}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{!gridDto ||
|
|
|
|
|
!dataSource ||
|
|
|
|
|
lookupReadyState.listFormCode !== listFormCode ||
|
|
|
|
|
!lookupReadyState.ready ? (
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
<Loading loading>{translate('::App.Loading')}</Loading>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="p-1">
|
|
|
|
|
<DxCardView
|
2026-07-16 14:01:18 +00:00
|
|
|
key={`CardView-${listFormCode}-${localizationRevision}`}
|
2026-07-16 08:19:17 +00:00
|
|
|
ref={cardViewRef as any}
|
|
|
|
|
fieldHintEnabled
|
|
|
|
|
id={`CardView-${listFormCode}`}
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
columns={cardColumns}
|
|
|
|
|
height={gridDto.gridOptions.height > 0 ? gridDto.gridOptions.height : undefined}
|
|
|
|
|
width={gridDto.gridOptions.width || '100%'}
|
|
|
|
|
cardMinWidth={280}
|
2026-07-16 14:01:18 +00:00
|
|
|
cardsPerRow={cardColumnCount}
|
|
|
|
|
allowColumnReordering={gridDto.gridOptions.columnOptionDto?.allowColumnReordering}
|
|
|
|
|
rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled}
|
2026-07-16 08:19:17 +00:00
|
|
|
className="list-card-view"
|
|
|
|
|
hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled}
|
|
|
|
|
remoteOperations={{ paging: true, filtering: true, sorting: true }}
|
|
|
|
|
wordWrapEnabled={gridDto.gridOptions.rowDto?.whiteSpace !== 'nowrap'}
|
|
|
|
|
onContentReady={onContentReady}
|
|
|
|
|
onSelectionChanged={onSelectionChanged}
|
|
|
|
|
onInitNewCard={(event) => {
|
2026-07-16 14:01:18 +00:00
|
|
|
const nextFormData = (event.data as Record<string, any>) ?? {}
|
|
|
|
|
formDataRef.current = nextFormData
|
|
|
|
|
setFormData(nextFormData)
|
2026-07-16 08:19:17 +00:00
|
|
|
setMode('new')
|
|
|
|
|
setIsPopupFullScreen(
|
|
|
|
|
useMobileEditPopup || (editingOptions?.popup?.fullScreen ?? false),
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
onEditingStart={(event) => {
|
2026-07-16 14:01:18 +00:00
|
|
|
const nextFormData = (event.data as Record<string, any>) ?? {}
|
|
|
|
|
formDataRef.current = nextFormData
|
|
|
|
|
setFormData(nextFormData)
|
2026-07-16 08:19:17 +00:00
|
|
|
setMode('edit')
|
|
|
|
|
setIsPopupFullScreen(
|
|
|
|
|
useMobileEditPopup || (editingOptions?.popup?.fullScreen ?? false),
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
onEditCanceled={() => {
|
|
|
|
|
setMode('view')
|
|
|
|
|
setIsPopupFullScreen(false)
|
|
|
|
|
}}
|
|
|
|
|
onSaved={() => {
|
|
|
|
|
setMode('view')
|
|
|
|
|
setIsPopupFullScreen(false)
|
|
|
|
|
}}
|
|
|
|
|
onDataErrorOccurred={(event) => {
|
|
|
|
|
toast.push(
|
|
|
|
|
<Notification type="danger" duration={2500}>
|
|
|
|
|
{String(event.error?.message ?? event.error ?? 'Select error')}
|
|
|
|
|
</Notification>,
|
|
|
|
|
{ placement: 'bottom-end' },
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
onCardInserted={() => refreshParentData?.()}
|
|
|
|
|
onCardUpdated={() => refreshParentData?.()}
|
|
|
|
|
onCardRemoved={() => refreshParentData?.()}
|
|
|
|
|
>
|
|
|
|
|
<CardHeader template={renderCardHeader} />
|
|
|
|
|
<Editing
|
|
|
|
|
allowAdding={
|
|
|
|
|
editingOptions?.allowAdding && checkPermission(gridDto.gridOptions.permissionDto?.c)
|
|
|
|
|
}
|
|
|
|
|
allowUpdating={
|
|
|
|
|
editingOptions?.allowUpdating &&
|
|
|
|
|
checkPermission(gridDto.gridOptions.permissionDto?.u)
|
|
|
|
|
}
|
|
|
|
|
allowDeleting={
|
|
|
|
|
editingOptions?.allowDeleting &&
|
|
|
|
|
checkPermission(gridDto.gridOptions.permissionDto?.d)
|
|
|
|
|
}
|
|
|
|
|
confirmDelete={editingOptions?.confirmDelete}
|
|
|
|
|
form={{
|
|
|
|
|
colCount: 1,
|
|
|
|
|
items: editingFormItems,
|
|
|
|
|
labelLocation: 'top',
|
|
|
|
|
onFieldDataChanged: onEditingFormFieldChanged,
|
|
|
|
|
showColonAfterLabel: false,
|
|
|
|
|
}}
|
|
|
|
|
popup={{
|
|
|
|
|
animation: {},
|
|
|
|
|
deferRendering: true,
|
|
|
|
|
wrapperAttr: useMobileEditPopup ? { class: 'mobile-edit-popup' } : undefined,
|
|
|
|
|
title:
|
|
|
|
|
(mode === 'new' ? '✚ ' : '🖊️ ') + translate('::' + editingOptions?.popup?.title),
|
|
|
|
|
showTitle: editingOptions?.popup?.showTitle,
|
|
|
|
|
hideOnOutsideClick: editingOptions?.popup?.hideOnOutsideClick,
|
|
|
|
|
width: useMobileEditPopup
|
|
|
|
|
? '100%'
|
|
|
|
|
: Number(editingOptions?.popup?.width) > 0
|
|
|
|
|
? editingOptions?.popup?.width
|
|
|
|
|
: 'auto',
|
|
|
|
|
maxWidth: useMobileEditPopup ? '100%' : '95vw',
|
|
|
|
|
height: getPopupHeight(useMobileEditPopup, isPopupFullScreen),
|
|
|
|
|
maxHeight: getPopupMaxHeight(
|
|
|
|
|
useMobileEditPopup,
|
|
|
|
|
isPopupFullScreen,
|
|
|
|
|
editingOptions?.popup?.height,
|
|
|
|
|
),
|
|
|
|
|
fullScreen: isPopupFullScreen,
|
|
|
|
|
position: useMobileEditPopup
|
|
|
|
|
? {
|
|
|
|
|
my: 'top center',
|
|
|
|
|
at: 'top center',
|
|
|
|
|
of: typeof window !== 'undefined' ? window : undefined,
|
|
|
|
|
}
|
|
|
|
|
: editingOptions?.popup?.position,
|
|
|
|
|
resizeEnabled: editingOptions?.popup?.resizeEnabled,
|
|
|
|
|
dragEnabled: editingOptions?.popup?.dragEnabled,
|
|
|
|
|
restorePosition: editingOptions?.popup?.restorePosition,
|
|
|
|
|
toolbarItems: [
|
|
|
|
|
{
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
toolbar: 'bottom',
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
text: translate('::Save'),
|
|
|
|
|
type: 'default',
|
|
|
|
|
onClick: () => cardViewRef.current?.instance().saveEditData(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
toolbar: 'bottom',
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
text: translate('::Cancel'),
|
|
|
|
|
onClick: () => cardViewRef.current?.instance().cancelEditData(),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
widget: 'dxButton',
|
|
|
|
|
toolbar: 'top',
|
|
|
|
|
location: 'after',
|
|
|
|
|
options: {
|
|
|
|
|
icon: isPopupFullScreen ? 'collapse' : 'fullscreen',
|
|
|
|
|
hint: isPopupFullScreen
|
|
|
|
|
? translate('::Normal Boyut')
|
|
|
|
|
: translate('::Tam Ekran'),
|
|
|
|
|
stylingMode: 'text',
|
|
|
|
|
onClick: () => setIsPopupFullScreen((current) => !current),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-07-16 14:01:18 +00:00
|
|
|
<Toolbar visible>
|
|
|
|
|
{cardColumnCountOptions.map((columnCount) => {
|
|
|
|
|
const selected = cardColumnCount === columnCount
|
|
|
|
|
return (
|
|
|
|
|
<ToolbarItem
|
|
|
|
|
key={`card-column-count-${columnCount}`}
|
|
|
|
|
location="before"
|
|
|
|
|
locateInMenu="never"
|
|
|
|
|
widget="dxButton"
|
|
|
|
|
options={{
|
|
|
|
|
text: String(columnCount),
|
|
|
|
|
hint: String(columnCount),
|
|
|
|
|
type: selected ? 'default' : 'normal',
|
|
|
|
|
stylingMode: selected ? 'contained' : 'text',
|
|
|
|
|
elementAttr: {
|
|
|
|
|
'aria-label': String(columnCount),
|
|
|
|
|
'aria-pressed': selected,
|
|
|
|
|
},
|
|
|
|
|
onClick: () => setCardColumnCount(columnCount),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
})}
|
2026-07-16 08:19:17 +00:00
|
|
|
{orderedToolbarData.map((item, index) => (
|
|
|
|
|
<ToolbarItem key={`${item.name ?? 'toolbar'}-${index}`} {...(item as any)} />
|
|
|
|
|
))}
|
|
|
|
|
{gridDto.gridOptions.extraFilterDto?.length ? (
|
|
|
|
|
<ToolbarItem
|
|
|
|
|
location="before"
|
|
|
|
|
render={() => (
|
|
|
|
|
<GridExtraFilterToolbar
|
|
|
|
|
filters={gridDto.gridOptions.extraFilterDto ?? []}
|
|
|
|
|
extraFilters={extraFilters}
|
|
|
|
|
setExtraFilters={setExtraFilters}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</Toolbar>
|
|
|
|
|
<Sorting mode={gridDto.gridOptions.sortMode} />
|
|
|
|
|
<SearchPanel
|
|
|
|
|
visible={gridDto.gridOptions.searchPanelDto.visible}
|
|
|
|
|
width={gridDto.gridOptions.searchPanelDto.width}
|
|
|
|
|
placeholder={translate('::App.Search')}
|
|
|
|
|
/>
|
2026-07-16 14:01:18 +00:00
|
|
|
<FilterPanel
|
|
|
|
|
visible={gridDto.gridOptions.filterPanelDto.visible}
|
|
|
|
|
filterEnabled={gridDto.gridOptions.filterPanelDto.filterEnabled}
|
|
|
|
|
texts={gridDto.gridOptions.filterPanelDto.texts}
|
|
|
|
|
/>
|
|
|
|
|
<HeaderFilter
|
|
|
|
|
visible={gridDto.gridOptions.headerFilterDto.visible}
|
|
|
|
|
width={gridDto.gridOptions.headerFilterDto.width}
|
|
|
|
|
height={gridDto.gridOptions.headerFilterDto.height}
|
|
|
|
|
search={{
|
|
|
|
|
enabled: gridDto.gridOptions.headerFilterDto.allowSearch,
|
|
|
|
|
timeout: gridDto.gridOptions.headerFilterDto.searchTimeout,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-07-16 08:19:17 +00:00
|
|
|
<Selection
|
|
|
|
|
mode={selectionOptions?.mode}
|
|
|
|
|
allowSelectAll={selectionOptions?.allowSelectAll}
|
|
|
|
|
selectAllMode={selectionOptions?.selectAllMode}
|
|
|
|
|
showCheckBoxesMode={selectionOptions?.showCheckBoxesMode}
|
|
|
|
|
/>
|
|
|
|
|
<Paging defaultPageSize={gridDto.gridOptions.pageSize ?? 20} />
|
|
|
|
|
<Pager
|
|
|
|
|
visible={gridDto.gridOptions.pagerOptionDto?.visible}
|
|
|
|
|
allowedPageSizes={gridDto.gridOptions.pagerOptionDto?.allowedPageSizes
|
|
|
|
|
?.split(',')
|
|
|
|
|
.map(Number)}
|
|
|
|
|
showPageSizeSelector={gridDto.gridOptions.pagerOptionDto?.showPageSizeSelector}
|
2026-07-16 14:01:18 +00:00
|
|
|
showInfo={gridDto.gridOptions.pagerOptionDto?.showInfo}
|
|
|
|
|
infoText={gridDto.gridOptions.pagerOptionDto?.infoText}
|
2026-07-16 08:19:17 +00:00
|
|
|
showNavigationButtons={gridDto.gridOptions.pagerOptionDto?.showNavigationButtons}
|
|
|
|
|
displayMode={gridDto.gridOptions.pagerOptionDto?.displayMode}
|
|
|
|
|
/>
|
|
|
|
|
<ColumnChooser
|
|
|
|
|
enabled={gridDto.gridOptions.columnOptionDto?.columnChooserEnabled}
|
|
|
|
|
mode={gridDto.gridOptions.columnOptionDto?.columnChooserMode}
|
|
|
|
|
/>
|
2026-07-16 14:01:18 +00:00
|
|
|
<LoadPanel
|
|
|
|
|
visible={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled !== false}
|
|
|
|
|
message={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
|
|
|
|
|
/>
|
2026-07-16 08:19:17 +00:00
|
|
|
</DxCardView>
|
|
|
|
|
|
|
|
|
|
{gridDto.gridOptions.subFormsDto?.length > 0 &&
|
|
|
|
|
gridDto.gridOptions.subFormsListFormType === SubFormTabTypeEnum.List && (
|
|
|
|
|
<>
|
|
|
|
|
<hr className="my-2" />
|
|
|
|
|
<SubForms gridDto={gridDto} formData={formData} level={level ?? 0} />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Dialog
|
|
|
|
|
width={1000}
|
|
|
|
|
isOpen={filterData.isImportModalOpen || false}
|
|
|
|
|
onClose={() => filterData.setIsImportModalOpen(false)}
|
|
|
|
|
onRequestClose={() => filterData.setIsImportModalOpen(false)}
|
|
|
|
|
>
|
|
|
|
|
<Dialog.Header className="-mt-5 border-b border-gray-200 p-2 dark:border-gray-700">
|
|
|
|
|
<h2 className="text-lg font-semibold">
|
|
|
|
|
{translate('::ListForms.ListForm.ImportManager')}
|
|
|
|
|
</h2>
|
|
|
|
|
</Dialog.Header>
|
|
|
|
|
<Dialog.Body className="mt-2 flex flex-col">
|
|
|
|
|
<ImportDashboard gridDto={gridDto} />
|
|
|
|
|
</Dialog.Body>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Dialog
|
|
|
|
|
isOpen={toolbarModalData?.open || false}
|
|
|
|
|
onClose={() => setToolbarModalData(undefined)}
|
|
|
|
|
onRequestClose={() => setToolbarModalData(undefined)}
|
|
|
|
|
>
|
|
|
|
|
{toolbarModalData?.content}
|
|
|
|
|
</Dialog>
|
|
|
|
|
<GridFilterDialogs gridRef={cardViewRef as any} listFormCode={listFormCode} {...filterData} />
|
|
|
|
|
{notePanelTarget && (
|
|
|
|
|
<NotePanel
|
|
|
|
|
isVisible
|
|
|
|
|
entityName={notePanelTarget.entityName}
|
|
|
|
|
entityId={notePanelTarget.entityId}
|
|
|
|
|
onToggle={() => setNotePanelTarget(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Container>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default CardView
|