1371 lines
50 KiB
TypeScript
1371 lines
50 KiB
TypeScript
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 {
|
||
ColumnFormatDto,
|
||
DbTypeEnum,
|
||
FieldCustomValueTypeEnum,
|
||
GridDto,
|
||
PlatformEditorTypes,
|
||
SubFormTabTypeEnum,
|
||
} from '@/proxy/form/models'
|
||
import { getList, getNextSequenceValue } from '@/services/form.service'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
import { usePermission } from '@/utils/hooks/usePermission'
|
||
import { useStoreActions, useStoreState } from '@/store'
|
||
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 type { DataType } from 'devextreme/common'
|
||
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 {
|
||
autoNumber,
|
||
extractSearchParamsFields,
|
||
getEditingFormGroups,
|
||
GridExtraFilterState,
|
||
safeJsonParse,
|
||
} from './Utils'
|
||
import {
|
||
buildEditPopupToolbarItems,
|
||
getEditPopupHeight,
|
||
getEditPopupMaxHeight,
|
||
getMobilePopupPosition,
|
||
shouldUseMobileEditPopup,
|
||
} from './shared/editPopup'
|
||
import { getValueByField, resolveEditorType } from './shared/editingForm'
|
||
import { createEditingFormHandlers } from './shared/editingFormScripts'
|
||
import { flattenColumns } from './shared/columns'
|
||
import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks'
|
||
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'
|
||
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||
|
||
interface CardViewProps {
|
||
listFormCode: string
|
||
searchParams?: URLSearchParams
|
||
isSubForm?: boolean
|
||
level?: number
|
||
refreshData?: () => Promise<void>
|
||
gridDto?: GridDto
|
||
}
|
||
|
||
const cardColumnCountOptions = [1, 2, 3, 4, 5] as const
|
||
const defaultCardColumnCount = 3
|
||
|
||
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')
|
||
const isHtmlColumn = column.colData?.editorType2 === 'dxHtmlEditor'
|
||
|
||
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
|
||
}
|
||
: isHtmlColumn
|
||
? (templateOptions: any) => {
|
||
const templateData = templateOptions?.model ?? templateOptions
|
||
const container = document.createElement('div')
|
||
container.innerHTML = String(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 getEditingFieldName = (dataField?: string | null) =>
|
||
String(dataField ?? '')
|
||
.split(':')[0]
|
||
.trim()
|
||
|
||
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 loadCascadeLookupDisplayValues = async (
|
||
columns: any[],
|
||
rows: Record<string, any>[],
|
||
) => {
|
||
const entries = await Promise.all(
|
||
columns
|
||
.filter((column) => column.colData?.lookupDto?.cascadeParentFields)
|
||
.map(async (column) => {
|
||
const lookup = column.colData.lookupDto
|
||
const parentFields: string[] = String(lookup.cascadeParentFields)
|
||
.split(',')
|
||
.map((field) => field.trim())
|
||
.filter(Boolean)
|
||
const contexts = [
|
||
...new Map(
|
||
rows.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
|
||
? pendingCount.then((count) => (count !== undefined ? count : originalTotalCount(options)))
|
||
: 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)
|
||
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)
|
||
const useMobileEditPopup = shouldUseMobileEditPopup()
|
||
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 widgetGroupRef = useRef<HTMLDivElement>(null)
|
||
const formDataRef = useRef<Record<string, any>>()
|
||
useEffect(() => {
|
||
formDataRef.current = formData
|
||
}, [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(
|
||
() =>
|
||
mergeFilterTriplets([
|
||
...extractSearchParamsFields(
|
||
safeJsonParse(defaultSearchFilterRef.current, null, 'Search filter parse error:'),
|
||
),
|
||
...extraFilters
|
||
.filter((filter) => !!filter.value)
|
||
.map(
|
||
(filter) => [filter.fieldName, filter.operator, filter.value] as [string, string, any],
|
||
),
|
||
]),
|
||
[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])
|
||
const setCardColumnCount = useCallback(
|
||
(cardColumnCount: number) => {
|
||
setListState({
|
||
listFormCode,
|
||
layout: savedListState?.layout ?? 'card',
|
||
cardColumnCount,
|
||
})
|
||
},
|
||
[listFormCode, savedListState?.layout, setListState],
|
||
)
|
||
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,
|
||
})
|
||
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
|
||
) {
|
||
listColumnsCacheRef.current = {
|
||
gridDto,
|
||
localizationRevision,
|
||
columns: flattenColumns(getBandedColumns() ?? []),
|
||
}
|
||
}
|
||
const listColumns = listColumnsCacheRef.current.columns
|
||
const cardColumnsCacheRef = useRef<{
|
||
listColumns?: any[]
|
||
columns: CardViewTypes.ColumnProperties[]
|
||
}>({ columns: [] })
|
||
if (cardColumnsCacheRef.current.listColumns !== listColumns) {
|
||
cardColumnsCacheRef.current = {
|
||
listColumns,
|
||
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) => {
|
||
const configuredDataField =
|
||
templateData?.dataField ??
|
||
templateData?.item?.dataField ??
|
||
templateData?.editorOptions?.name ??
|
||
templateData?.name
|
||
const dataField = getEditingFieldName(configuredDataField)
|
||
const column = listColumnsCacheRef.current.columns.find(
|
||
(candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
|
||
)
|
||
const component = templateData?.component
|
||
const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
|
||
const editorValue = templateData?.editorOptions?.value
|
||
|
||
return (
|
||
<ImageUploadEditorComponent
|
||
{...templateData}
|
||
column={column}
|
||
value={
|
||
editorValue !== undefined ? editorValue : getValueByField(currentFormData, dataField)
|
||
}
|
||
setValue={(value: any) => {
|
||
if (!dataField) return
|
||
if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
|
||
templateData.editorOptions.onValueChanged({ value })
|
||
} else {
|
||
component?.updateData?.(dataField, value)
|
||
}
|
||
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)
|
||
const disabled = parentFields?.some((field: string) => !getValueByField(nextData, field))
|
||
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
|
||
},
|
||
[cascadeChildrenByParent],
|
||
)
|
||
|
||
// Grid/Tree/Scheduler ile aynı editorScript altyapısı; CardView'ın New ve Edit
|
||
// popup formlarında da script'ler çalışsın diye bağlanır.
|
||
const editingFormDataRef = useRef<Record<string, any>>({})
|
||
const editingFormInstanceRef = useRef<any>()
|
||
const editorScriptHandlers = useMemo(
|
||
() =>
|
||
createEditingFormHandlers({
|
||
getGridDto: () => gridDto,
|
||
getGridInstance: () => cardViewRef.current?.instance?.(),
|
||
editingFormDataRef,
|
||
editingFormInstanceRef,
|
||
}),
|
||
[gridDto],
|
||
)
|
||
|
||
const onEditingFormContentReady = useCallback(
|
||
(event: any) => {
|
||
editingFormInstanceRef.current = event.component
|
||
editorScriptHandlers.onContentReady(event)
|
||
},
|
||
[editorScriptHandlers],
|
||
)
|
||
|
||
const onEditingFormDataChanged = useCallback(
|
||
(event: any) => {
|
||
// Önce cascade temizliği, sonra alanın kendi script'i.
|
||
onEditingFormFieldChanged(event)
|
||
editorScriptHandlers.onFieldDataChanged(event)
|
||
},
|
||
[editorScriptHandlers, onEditingFormFieldChanged],
|
||
)
|
||
|
||
const applyLookupDisplayValues = useCallback((values: Record<string, Map<string, string>>) => {
|
||
if (lookupDisplayValuesEqual(lookupDisplayValuesRef.current, values)) return
|
||
lookupDisplayValuesRef.current = values
|
||
}, [])
|
||
|
||
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((values) => {
|
||
if (!active) return
|
||
applyLookupDisplayValues(values)
|
||
setLookupReadyState((current) =>
|
||
current.listFormCode === listFormCode && current.ready
|
||
? current
|
||
: { listFormCode, ready: true },
|
||
)
|
||
})
|
||
.catch((error) => {
|
||
console.error('CardView lookup preload error:', error)
|
||
if (active) {
|
||
setLookupReadyState((current) =>
|
||
current.listFormCode === listFormCode && current.ready
|
||
? current
|
||
: { 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),
|
||
]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues }))
|
||
}, [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: ColumnFormatDto) => {
|
||
const fieldName = item.fieldName!
|
||
const column = item
|
||
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.fieldName, 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,
|
||
}
|
||
}
|
||
|
||
// Grid ve Form'da editorOptions.placeholder kolonun PlaceHolder alanını ezer;
|
||
// burada tersi olduğu için aynı yapılandırma CardView'da farklı davranıyordu.
|
||
if (column.placeHolder && editorOptions.placeholder === undefined) {
|
||
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: formDataRef.current ?? {} })
|
||
: lookup.dataSource,
|
||
displayExpr: lookup.displayExpr,
|
||
valueExpr: lookup.valueExpr,
|
||
}
|
||
}
|
||
|
||
return {
|
||
itemType: 'simple',
|
||
dataField: fieldName,
|
||
name: fieldName,
|
||
colSpan: item.colSpan,
|
||
label: {
|
||
text: item.fieldName!.includes(':')
|
||
? item.fieldName!.split(':')[1]
|
||
: translate('::' + (column.captionName || column.fieldName)),
|
||
},
|
||
editorType: resolveEditorType(item.editorType2),
|
||
editorOptions,
|
||
template:
|
||
item.editorType2 === PlatformEditorTypes.dxImageUpload
|
||
? imageUploadFormTemplate
|
||
: item.editorType2 === PlatformEditorTypes.dxImageViewer
|
||
? imageViewerFormTemplate
|
||
: undefined,
|
||
}
|
||
},
|
||
[gridDto, imageUploadFormTemplate, imageViewerFormTemplate, mode, translate],
|
||
)
|
||
|
||
const editingFormItems = useMemo(() => {
|
||
if (!gridDto || mode === 'view') return []
|
||
|
||
const groups = [...(getEditingFormGroups(gridDto) ?? [])].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.editOrderNo ?? 0) - (right.editOrderNo ?? 0))
|
||
.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.editOrderNo ?? 0) - (right.editOrderNo ?? 0))
|
||
.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])
|
||
|
||
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(),
|
||
})
|
||
}
|
||
})
|
||
actionsElement.appendChild(button)
|
||
})
|
||
|
||
return actionsElement
|
||
}, [])
|
||
|
||
const editingOptions = gridDto?.gridOptions.editingOptionDto
|
||
const selectionOptions = gridDto?.gridOptions.selectionDto
|
||
|
||
const onInitNewCard = useCallback(
|
||
(event: CardViewTypes.InitNewCardEvent<Record<string, any>>) => {
|
||
const nextFormData = event.data as Record<string, any>
|
||
formDataRef.current = nextFormData
|
||
|
||
setMode('new')
|
||
setIsPopupFullScreen(useMobileEditPopup || (editingOptions?.popup?.fullScreen ?? false))
|
||
|
||
event.promise = (async () => {
|
||
const rawFilter = searchParamsRef.current?.get('filter')
|
||
let filters: any[] = []
|
||
|
||
if (rawFilter) {
|
||
try {
|
||
filters = extractSearchParamsFields(JSON.parse(rawFilter))
|
||
} catch (error) {
|
||
console.error('Filter parse edilemedi:', error)
|
||
}
|
||
}
|
||
|
||
for (const column of gridDto?.columnFormats ?? []) {
|
||
const fieldName = column.fieldName
|
||
if (!fieldName || extraFilters.some((filter) => filter.fieldName === fieldName)) {
|
||
continue
|
||
}
|
||
|
||
if (column.defaultValue !== null && column.defaultValue !== undefined) {
|
||
if (typeof column.defaultValue === 'string' && column.defaultValue === '@AUTONUMBER') {
|
||
nextFormData[fieldName] = autoNumber()
|
||
} else if (column.defaultValueType === FieldCustomValueTypeEnum.Sequence) {
|
||
try {
|
||
const response = await getNextSequenceValue(String(column.defaultValue))
|
||
nextFormData[fieldName] = response.data
|
||
} catch (error) {
|
||
console.error('Sequence default value alınamadı:', {
|
||
fieldName,
|
||
defaultValue: column.defaultValue,
|
||
error,
|
||
})
|
||
}
|
||
} else {
|
||
nextFormData[fieldName] = column.defaultValue
|
||
}
|
||
}
|
||
|
||
const fieldMatch = filters.find(([field]) => field === fieldName)
|
||
if (!fieldMatch) continue
|
||
|
||
const value = fieldMatch[2]
|
||
switch (column.dataType as DataType) {
|
||
case 'date':
|
||
case 'datetime':
|
||
nextFormData[fieldName] = new Date(value)
|
||
break
|
||
case 'number':
|
||
nextFormData[fieldName] = Number(value)
|
||
break
|
||
case 'boolean':
|
||
nextFormData[fieldName] = value === true || value === 'true'
|
||
break
|
||
case 'object':
|
||
try {
|
||
nextFormData[fieldName] = JSON.parse(value)
|
||
} catch {
|
||
nextFormData[fieldName] = value
|
||
}
|
||
break
|
||
default:
|
||
nextFormData[fieldName] = value
|
||
break
|
||
}
|
||
}
|
||
|
||
formDataRef.current = nextFormData
|
||
setFormData({ ...nextFormData })
|
||
})()
|
||
},
|
||
[editingOptions?.popup?.fullScreen, extraFilters, gridDto, useMobileEditPopup],
|
||
)
|
||
|
||
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 (
|
||
<>
|
||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
||
</div>
|
||
|
||
<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
|
||
key={`CardView-${listFormCode}-${localizationRevision}`}
|
||
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}
|
||
cardsPerRow={cardColumnCount}
|
||
allowColumnReordering={gridDto.gridOptions.columnOptionDto?.allowColumnReordering}
|
||
rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled}
|
||
className="list-card-view list-view-toolbar-host"
|
||
hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled}
|
||
remoteOperations={{ paging: true, filtering: true, sorting: true }}
|
||
wordWrapEnabled={gridDto.gridOptions.rowDto?.whiteSpace !== 'nowrap'}
|
||
onContentReady={onContentReady}
|
||
onSelectionChanged={onSelectionChanged}
|
||
onInitNewCard={onInitNewCard}
|
||
onEditingStart={(event) => {
|
||
const nextFormData = (event.data as Record<string, any>) ?? {}
|
||
formDataRef.current = nextFormData
|
||
setFormData(nextFormData)
|
||
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',
|
||
onContentReady: onEditingFormContentReady,
|
||
onFieldDataChanged: onEditingFormDataChanged,
|
||
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: getEditPopupHeight(useMobileEditPopup, isPopupFullScreen),
|
||
maxHeight: getEditPopupMaxHeight(
|
||
useMobileEditPopup,
|
||
isPopupFullScreen,
|
||
editingOptions?.popup?.height,
|
||
),
|
||
fullScreen: isPopupFullScreen,
|
||
position: useMobileEditPopup
|
||
? getMobilePopupPosition()
|
||
: editingOptions?.popup?.position,
|
||
resizeEnabled: editingOptions?.popup?.resizeEnabled,
|
||
dragEnabled: editingOptions?.popup?.dragEnabled,
|
||
restorePosition: editingOptions?.popup?.restorePosition,
|
||
toolbarItems: buildEditPopupToolbarItems(translate, isPopupFullScreen, {
|
||
onSave: () => cardViewRef.current?.instance().saveEditData(),
|
||
onCancel: () => cardViewRef.current?.instance().cancelEditData(),
|
||
onToggleFullScreen: () => setIsPopupFullScreen((current) => !current),
|
||
}),
|
||
}}
|
||
/>
|
||
<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),
|
||
}}
|
||
/>
|
||
)
|
||
})}
|
||
{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')}
|
||
/>
|
||
<FilterPanel
|
||
visible={gridDto.gridOptions.filterPanelDto.visible}
|
||
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,
|
||
}}
|
||
/>
|
||
<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}
|
||
showInfo={gridDto.gridOptions.pagerOptionDto?.showInfo}
|
||
infoText={gridDto.gridOptions.pagerOptionDto?.infoText}
|
||
showNavigationButtons={gridDto.gridOptions.pagerOptionDto?.showNavigationButtons}
|
||
displayMode={gridDto.gridOptions.pagerOptionDto?.displayMode}
|
||
/>
|
||
<ColumnChooser
|
||
enabled={gridDto.gridOptions.columnOptionDto?.columnChooserEnabled}
|
||
mode={gridDto.gridOptions.columnOptionDto?.columnChooserMode}
|
||
/>
|
||
<LoadPanel
|
||
visible={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled !== false}
|
||
message={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
|
||
/>
|
||
</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
|