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, 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' 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 { 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, getEditingFormGroups, 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 gridDto?: GridDto } const cardColumnCountOptions = [1, 2, 3, 4, 5] as const const defaultCardColumnCount = 3 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> }, ): 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 getValueByField = (data: Record, 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 } const getEditingFieldName = (dataField?: string | null) => String(dataField ?? '') .split(':')[0] .trim() const lookupDisplayValuesEqual = ( left: Record>, right: Record>, ) => { 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[], knownValues: Record> = {}, ) => { 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 parentValueLists: string[][] = parentFields.map((field) => { 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[]>( (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 } | undefined)?.store return ((await store?.load?.()) ?? []) as Record[] }), ) const values = new Map() 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> } 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) => { const cardStore = store as any const originalLoad = cardStore._loadFunc?.bind(cardStore) if (!originalLoad) return store const cachedLoads = new Map() const pendingLoads = new Map>() const pendingTotalCounts = new Map>() const totalCounts = new Map() 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 = isMobileViewport() || isTouchLikeDevice() const cardViewRef = useRef() const searchParamsRef = useRef(searchParams) const defaultSearchFilterRef = useRef(searchParams?.get('filter') ?? null) const dataSourceSearchParamsRef = useRef() 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('view') const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [formData, setFormData] = useState>() const formDataRef = useRef>() formDataRef.current = formData const lookupDisplayValuesRef = useRef>>({}) const [lookupReadyState, setLookupReadyState] = useState({ listFormCode, ready: false }) const [notePanelTarget, setNotePanelTarget] = useState<{ entityName: string entityId: string } | null>(null) const [extraFilters, setExtraFilters] = useState(() => 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((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]) 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) => { 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 ( { 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) => ( ), [], ) 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() 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 setFormData(nextData) }, [cascadeChildrenByParent], ) const applyLookupDisplayValues = useCallback((values: Record>) => { 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(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), ]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues })) }, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues]) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef }) const dataSource = useMemo | 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 = {} 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, } } 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', dataField: fieldName, name: fieldName, colSpan: item.colSpan, label: { text: item.fieldName!.includes(':') ? item.fieldName!.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 = [...(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 onSelectionChanged = useCallback( (event: CardViewTypes.SelectionChangedEvent) => { const selectedCardsData = event.selectedCardsData as Record[] 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 ( {!isSubForm && ( )} {!gridDto || !dataSource || lookupReadyState.listFormCode !== listFormCode || !lookupReadyState.ready ? (
{translate('::App.Loading')}
) : (
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={(event) => { const nextFormData = (event.data as Record) ?? {} formDataRef.current = nextFormData setFormData(nextFormData) setMode('new') setIsPopupFullScreen( useMobileEditPopup || (editingOptions?.popup?.fullScreen ?? false), ) }} onEditingStart={(event) => { const nextFormData = (event.data as Record) ?? {} 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( {String(event.error?.message ?? event.error ?? 'Select error')} , { placement: 'bottom-end' }, ) }} onCardInserted={() => refreshParentData?.()} onCardUpdated={() => refreshParentData?.()} onCardRemoved={() => refreshParentData?.()} > 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), }, }, ], }} /> {cardColumnCountOptions.map((columnCount) => { const selected = cardColumnCount === columnCount return ( setCardColumnCount(columnCount), }} /> ) })} {orderedToolbarData.map((item, index) => ( ))} {gridDto.gridOptions.extraFilterDto?.length ? ( ( )} /> ) : null} {gridDto.gridOptions.subFormsDto?.length > 0 && gridDto.gridOptions.subFormsListFormType === SubFormTabTypeEnum.List && ( <>
)} filterData.setIsImportModalOpen(false)} onRequestClose={() => filterData.setIsImportModalOpen(false)} >

{translate('::ListForms.ListForm.ImportManager')}

)} setToolbarModalData(undefined)} onRequestClose={() => setToolbarModalData(undefined)} > {toolbarModalData?.content} {notePanelTarget && ( setNotePanelTarget(null)} /> )}
) } export default CardView