From 4bec5442a8aa232486b512fbe7ea0b9850d92ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:56:21 +0300 Subject: [PATCH] =?UTF-8?q?CardView=20d=C3=BCzenlemesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/views/list/CardView.tsx | 1090 +++++++---------- ui/src/views/list/Grid.tsx | 1 - .../views/list/useListFormCustomDataSource.ts | 4 +- 3 files changed, 426 insertions(+), 669 deletions(-) diff --git a/ui/src/views/list/CardView.tsx b/ui/src/views/list/CardView.tsx index 6ac06781..b469ddf8 100644 --- a/ui/src/views/list/CardView.tsx +++ b/ui/src/views/list/CardView.tsx @@ -1,32 +1,11 @@ -// URLSearchParams'ı primitive objeye çeviren yardımcı fonksiyon -function getSearchParamsObject(searchParams?: URLSearchParams) { - if (!searchParams) return undefined; - const obj: Record = {}; - for (const [key, value] of searchParams.entries()) { - obj[key] = value; - } - return obj; -} import Container from '@/components/shared/Container' -import { Notification, toast } from '@/components/ui' import { DX_CLASSNAMES } from '@/constants/app.constant' -import { - DbTypeEnum, - GridDto, - ListFormCustomizationTypeEnum, - PlatformEditorTypes, - UiLookupDataSourceTypeEnum, -} from '@/proxy/form/models' -import { - getListFormCustomization, - postListFormCustomization, -} from '@/services/list-form-customization.service' +import { DbTypeEnum, GridDto, PlatformEditorTypes } from '@/proxy/form/models' import { useLocalization } from '@/utils/hooks/useLocalization' import { usePermission } from '@/utils/hooks/usePermission' import { usePWA } from '@/utils/hooks/usePWA' import CardViewDx, { CardViewRef, - Column, ColumnChooser, Editing, HeaderFilter, @@ -37,23 +16,19 @@ import CardViewDx, { Toolbar, Selection, FilterPanel, + CardViewTypes, } from 'devextreme-react/card-view' import type { CardInsertingEvent, CardUpdatingEvent, EditingStartEvent, - InitNewCardEvent, - SelectionChangedEvent, } from 'devextreme/ui/card_view' -import type { DataErrorOccurredInfo } from 'devextreme/common/grids' -import type { EventInfo } from 'devextreme/events' import { EditingFormItemDto } from '@/proxy/form/models' import { captionize } from 'devextreme/core/utils/inflector' import CustomStore from 'devextreme/data/custom_store' -import DataSource from 'devextreme/data/data_source' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Helmet } from 'react-helmet' -import { RowMode } from '../form/types' +import { RowMode, SimpleItemWithColData } from '../form/types' import { GridColumnData } from './GridColumnData' import { addCss, @@ -71,9 +46,13 @@ import { layoutTypes } from '../admin/listForm/edit/types' import { useListFormCustomDataSource } from './useListFormCustomDataSource' import { useListFormColumns } from './useListFormColumns' import { Loading } from '@/components/shared' -import { useStoreState } from '@/store' +import { useStoreActions, useStoreState } from '@/store' import { ROUTES_ENUM } from '@/routes/route.constant' -import { dynamicFetch } from '@/services/form.service' +import { useFilters } from './useFilters' +import { DataType } from 'devextreme/common' +import { Template } from 'devextreme-react' +import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' +import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' interface CardViewProps { listFormCode: string @@ -98,7 +77,8 @@ const CardView = (props: CardViewProps) => { const refListFormCode = useRef('') const widgetGroupRef = useRef(null) - const [columnData, setColumnData] = useState() + const [cardViewDataSource, setCardViewDataSource] = useState>() + const [columnData, setColumnData] = useState() const [formData, setFormData] = useState() const [mode, setMode] = useState('view') const [extraFilters, setExtraFilters] = useState([]) @@ -106,11 +86,25 @@ const CardView = (props: CardViewProps) => { const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [cardsPerRow, setCardsPerRow] = useState(0) + + // Store'dan admin.lists.states'e erişim + const adminStates = useStoreState((state: any) => state.admin?.lists?.states || []) + const setAdminStates = useStoreActions((actions: any) => actions.admin?.lists?.setStates) const [pageSize, setPageSize] = useState(20) - const [lookupItemsCache, setLookupItemsCache] = useState>(new Map()) + + type EditorOptionsWithButtons = { + buttons?: any[] + } & Record const defaultSearchParams = useRef(null) + const settingButtonClick = useCallback(() => { + window.open( + ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode), + isPwaMode ? '_self' : '_blank', + ) + }, [listFormCode, isPwaMode]) + useEffect(() => { const initializeCardView = async () => { const response = await getList({ listFormCode }) @@ -130,6 +124,12 @@ const CardView = (props: CardViewProps) => { } }, [searchParams]) + const { filterToolbarData, ...filterData } = useFilters({ + gridDto, + gridRef: cardViewRef as any, + listFormCode, + }) + const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef as any }) const { getBandedColumns } = useListFormColumns({ gridDto, @@ -138,150 +138,7 @@ const CardView = (props: CardViewProps) => { gridRef: cardViewRef as any, }) - // Lookup data source helpers - const createLookupStaticDataSource = useCallback( - (load: () => any, filter: any = null, key: any = 'static', sort: any = 'name') => - new DataSource({ - store: new CustomStore({ - key, - loadMode: 'raw', - load: async () => { - return Promise.resolve(load()).then((res) => res ?? []) - }, - }), - paginate: false, - sort, - filter, - }), - [], - ) - - const createLookupQueryDataSource = useCallback( - (listFormCode?: string, listFormFieldName?: string, filters?: any[]) => { - return new DataSource({ - store: new CustomStore({ - loadMode: 'raw', - load: async () => { - try { - const response = await dynamicFetch('list-form-select/lookup', 'POST', null, { - listFormCode, - listFormFieldName, - filters, - }) - - return (response.data ?? []).map((a: any) => ({ - key: a.Key, - name: a.Name, - group: a.Group, - ...a, - })) - } catch (error: any) { - return [] - } - }, - }), - paginate: false, - }) - }, - [], - ) - - const createLookupApiDataSource = useCallback( - (listFormCode?: string, lookupQuery?: string, filters?: any[], keyName?: string) => { - return new DataSource({ - store: new CustomStore({ - key: keyName, - loadMode: 'raw', - load: async () => { - if (!lookupQuery) return [] - - const [method, url, body, keySelector, nameSelector, groupSelector] = - lookupQuery.split(';') - - let resolvedBody = body - if (filters?.length) { - for (let i = 0; i < filters.length; i++) { - resolvedBody = resolvedBody.replace( - new RegExp(`@param${i}`, 'g'), - String(filters[i]), - ) - } - } - - try { - const response = await dynamicFetch(url, method, null, resolvedBody) - let { data } = response - if (!data) return [] - if (!Array.isArray(data)) data = [data] - return data.map((a: any) => ({ - key: eval(keySelector), - name: eval(nameSelector), - group: eval(groupSelector), - ...a, - })) - } catch { - return [] - } - }, - }), - paginate: false, - }) - }, - [], - ) - - const lookupDataSource = useCallback( - (options: any, colData: any) => { - const { lookupDto } = colData - const filters: any[] = [] - - if (lookupDto.cascadeParentFields) { - if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) { - filters.push([ - lookupDto?.cascadeRelationField, - lookupDto?.cascadeFilterOperator, - options?.data?.[lookupDto?.cascadeParentField], - ]) - } else { - const data = options?.data ?? options - for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) { - filters.push(data?.[cascadeParentField]) - } - } - } - - if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) { - return createLookupStaticDataSource( - () => JSON.parse(lookupDto?.lookupQuery), - filters.length ? filters : null, - `static:${listFormCode}:${colData.fieldName}`, - ) - } else if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.Query) { - return createLookupQueryDataSource(listFormCode, colData.fieldName, filters) - } else if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.WebService) { - return createLookupApiDataSource( - listFormCode, - lookupDto?.lookupQuery, - filters, - colData.lookupDto?.valueExpr?.toLowerCase(), - ) - } - return { store: [] } - }, - [ - listFormCode, - createLookupStaticDataSource, - createLookupQueryDataSource, - createLookupApiDataSource, - ], - ) - function refreshData() { - // Cache'i temizle - if (typeof (window as any).__clearCardViewCache === 'function') { - ;(window as any).__clearCardViewCache() - } - const instance = cardViewRef.current?.instance() if (instance) { instance.getDataSource()?.reload() @@ -289,19 +146,36 @@ const CardView = (props: CardViewProps) => { } // CardView specific events - function onSelectionChanged(data: SelectionChangedEvent) { + function onSelectionChanged(data: any) { const grdOpt = gridDto?.gridOptions - const cardView = cardViewRef.current?.instance() - if (!grdOpt || !cardView) { + const grd = cardViewRef.current?.instance() + if (!grdOpt || !grd) { return } - if (data.selectedCardsData?.length) { - setFormData(data.selectedCardsData[0]) + // kullanicinin yetkisi varsa ve birden fazla kayit secili ise coklu silme gorunsun + if (grdOpt.editingOptionDto?.allowDeleting) { + // && abp.auth.isGranted(grdOpt.permissionDto?.d) + // kullanicinin silme yetkisi var ise + const opt = grd.option('toolbar') + const deleteSelectedRecordsIndex = opt?.items + ?.map((e: any) => e.name) + .indexOf('deleteSelectedRecords') + // deleteSelectedRecords ismindeki custom butonun index degerini bul + + grd.option( + `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, + data.selectedRowsData.length > 1, + ) // birden fazla kayit secilmis ise gorunsun + } + + // SubForm'ları gösterebilmek için secili satiri formData'ya at + if (data.selectedRowsData.length) { + setFormData(data.selectedRowsData[0]) } } - function onInitNewCard(e: InitNewCardEvent) { + function onInitNewRow(e: any) { if (!gridDto?.columnFormats) { return } @@ -316,8 +190,10 @@ const CardView = (props: CardViewProps) => { // Grid'den gelen columnFormat'ları kullanarak default değerleri set et if (colFormat.defaultValue != null) { - const defaultValStr = String(colFormat.defaultValue) - if (defaultValStr === '@AUTONUMBER') { + if ( + typeof colFormat.defaultValue === 'string' && + colFormat.defaultValue === '@AUTONUMBER' + ) { e.data[colFormat.fieldName] = autoNumber() } else { e.data[colFormat.fieldName] = colFormat.defaultValue @@ -330,85 +206,120 @@ const CardView = (props: CardViewProps) => { } // URL'den veya Component Prop'dan gelen parametreleri set et - const defaultValue = searchParams?.get(colFormat.fieldName) - if (defaultValue) { - e.data[colFormat.fieldName] = defaultValue + if (!searchParams) { + continue + } + + const rawFilter = searchParams?.get('filter') + if (rawFilter) { + const parsed = JSON.parse(rawFilter) + const filters = extractSearchParamsFields(parsed) + const fieldMatch = filters.find(([field]) => field === colFormat.fieldName) + + if (fieldMatch) { + const val = fieldMatch[2] //value + const dType = colFormat.dataType as DataType + + switch (dType) { + case 'date': + case 'datetime': + e.data[colFormat.fieldName] = new Date(val) + break + case 'number': + e.data[colFormat.fieldName] = Number(val) + break + case 'boolean': + e.data[colFormat.fieldName] = val === true || val === 'true' + break + case 'object': + try { + e.data[colFormat.fieldName] = JSON.parse(val) + } catch {} + break + default: + e.data[colFormat.fieldName] = val + break + } + } } } } - function onCardInserting(e: CardInsertingEvent) { + function onRowInserting(e: CardInsertingEvent) { e.data = setFormEditingExtraItemValues(e.data) } - function onCardUpdating(e: CardUpdatingEvent) { + function onRowUpdating(e: CardUpdatingEvent) { if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) { - e.newData = { - ...e.newData, - [gridDto?.gridOptions.keyFieldName!]: e.oldData[gridDto?.gridOptions.keyFieldName!], + if (Object.keys(e.newData).some((a) => a.includes(':'))) { + Object.keys(e.oldData).forEach((col) => { + if (col.includes(':')) { + e.newData[col] = e.newData[col] ?? e.oldData[col] + } + }) } + e.newData = setFormEditingExtraItemValues(e.newData) } else { - e.newData = { - ...e.oldData, - ...e.newData, - [gridDto?.gridOptions.keyFieldName!]: e.oldData[gridDto?.gridOptions.keyFieldName!], - } + let newData = { ...e.oldData, ...e.newData } + newData = setFormEditingExtraItemValues(newData) + Object.keys(newData).forEach((key) => { + if (key.includes(':')) { + delete newData[key] + } + }) + e.newData = newData } if (gridDto?.gridOptions.keyFieldName) { - e.newData = setFormEditingExtraItemValues(e.newData) + delete e.newData[gridDto?.gridOptions.keyFieldName] } } function onEditingStart(e: EditingStartEvent) { setMode('edit') setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) - } - - function onDataErrorOccurred(e: EventInfo & DataErrorOccurredInfo) { - toast.push( - - {(e.error as any)?.message || 'An error occurred'} - , - { - placement: 'top-end', - }, - ) - } - - const customSaveState = useCallback( - (state: any) => { - return postListFormCustomization({ - listFormCode: listFormCode, - customizationType: ListFormCustomizationTypeEnum.GridState, - filterName: `cardview-state`, - customizationData: JSON.stringify(state), - }).then(() => { - setGridPanelColor(statedGridPanelColor) - }) - }, - [listFormCode], - ) - - const customLoadState = useCallback(() => { - return getListFormCustomization( - listFormCode, - ListFormCustomizationTypeEnum.GridState, - `cardview-state`, - ).then((response: any) => { - setGridPanelColor(statedGridPanelColor) - if (response.data?.length > 0) { - return JSON.parse(response.data[0].customizationData) + const columns = e.component.option('columns') as GridColumnData[] + // FormEditingExtraItem field ise datayı doldur + columns?.forEach((col) => { + if (!col.dataField?.includes(':')) { + return } + const field = col.dataField.split(':') + if (!e.data[field[0]]) { + return + } + const json = JSON.parse(e.data[field[0]]) + e.data[col.dataField] = json[field[1]] }) - }, [listFormCode]) + } useEffect(() => { + if (cardViewRef?.current) { + const instance = cardViewRef?.current?.instance() + if (instance) { + instance.option('columns', undefined) + instance.option('remoteOperations', false) + instance.option('dataSource', undefined) + } + } + if (refListFormCode.current !== listFormCode) { - setColumnData(undefined) + setExtraFilters([]) } }, [listFormCode]) + // cardsPerRow başlangıç değeri: önce admin.lists.states'ten, yoksa gridDto'dan + useEffect(() => { + let foundState = adminStates.find((s: any) => s.listFormCode === listFormCode) + if (foundState) { + setCardsPerRow(foundState.cardLayoutColumn) + } else if (gridDto?.gridOptions.layoutDto?.cardLayoutColumn) { + setCardsPerRow(gridDto.gridOptions.layoutDto.cardLayoutColumn) + } else { + setCardsPerRow(0) + } + }, [listFormCode, adminStates]) + useEffect(() => { if (!gridDto) { return @@ -428,232 +339,102 @@ const CardView = (props: CardViewProps) => { } if (gridDto?.gridOptions.extraFilterDto) { - const extras = gridDto.gridOptions.extraFilterDto.map((f) => ({ - fieldName: f.fieldName, - caption: f.caption, - operator: f.operator || '=', - value: f.defaultValue || '', - controlType: f.controlType, - })) - // Sadece ilk yüklemede extraFilters'ı set et, her gridDto değişiminde değil - setExtraFilters((prev) => { - if (prev.length === 0) return extras - return prev - }) + setExtraFilters( + gridDto.gridOptions.extraFilterDto.map((f) => ({ + fieldName: f.fieldName, + operator: f.operator, + controlType: f.controlType, + value: f.defaultValue ?? '', + })), + ) } - if (gridDto?.gridOptions.editingOptionDto?.popup) { - setIsPopupFullScreen(gridDto.gridOptions.editingOptionDto.popup.fullScreen) + if (gridDto.gridOptions.editingOptionDto?.popup) { + setIsPopupFullScreen(gridDto.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) } + }, [gridDto]) - // cardsPerRow başlangıç değeri - localStorage'dan oku - const storageKey = `cardview-cardsPerRow-${listFormCode}` - const savedCardsPerRow = localStorage.getItem(storageKey) - - if (savedCardsPerRow !== null) { - setCardsPerRow(parseInt(savedCardsPerRow, 10)) - } else if (gridDto?.gridOptions.layoutDto?.cardLayoutColumn) { - setCardsPerRow(gridDto.gridOptions.layoutDto.cardLayoutColumn) - } - - // pageSize başlangıç değeri - if (gridDto?.gridOptions.pageSize) { - setPageSize(gridDto.gridOptions.pageSize) - } - }, [gridDto, listFormCode]) - - // Lookup DataSource'ları bir kere oluştur ve cache'le - const lookupDataSourcesRef = useRef>(new Map()) - - // gridDto değiştiğinde lookup cache'i temizle - useEffect(() => { - lookupDataSourcesRef.current.clear() - }, [gridDto, listFormCode]) - - // Build columns with lookup support - sadece gridDto değiştiğinde - const columnsWithLookup = useMemo(() => { - if (!gridDto || !config) return [] - - const cols = getBandedColumns() - if (!cols) return [] - - return cols.map((col) => { - const colData = gridDto.columnFormats.find((c) => c.fieldName === col.dataField) - if (!colData) return col - - // Type assertion for extended column properties - const extCol = col as any - - // Form items için editorType ve editorOptions ayarla - const gridFormItem = gridDto.gridOptions.editingFormDto - ?.flatMap((f) => f.items) - .find((i) => i?.dataField === col.dataField) - - // Lookup desteği ekle - dataSourceType > 0 olmalı (0 = None) - if (colData.lookupDto?.dataSourceType && colData.lookupDto.dataSourceType > 0) { - // Lookup olduğunu işaretle (renderColumns'da kullanılacak) - ;(col as any).isLookup = true - ;(col as any).lookupInfo = { - valueExpr: colData.lookupDto?.valueExpr?.toLowerCase() || 'key', - displayExpr: colData.lookupDto?.displayExpr?.toLowerCase() || 'name', - } - } - - // Form item ayarları - if (gridFormItem) { - const editorOptions: any = {} - - // Parse editorOptions from JSON - if (gridFormItem.editorOptions) { - try { - Object.assign(editorOptions, JSON.parse(gridFormItem.editorOptions)) - } catch {} - } - - // EditorType belirleme (SchedulerView pattern'i) - let editorType: any = gridFormItem.editorType2 || gridFormItem.editorType - if (gridFormItem.editorType2 === PlatformEditorTypes.dxGridBox) { - editorType = 'dxDropDownBox' - } else if (gridFormItem.editorType2 === PlatformEditorTypes.dxTagBox) { - editorType = 'dxTagBox' - } else if (gridFormItem.editorType2) { - editorType = gridFormItem.editorType2 - } - - // Lookup için dataSource ve valueExpr/displayExpr ekle - dataSourceType > 0 olmalı - if (colData.lookupDto?.dataSourceType && colData.lookupDto.dataSourceType > 0) { - // Cache'den al - const cacheKey = `${colData.fieldName}` - const lookupDs = lookupDataSourcesRef.current.get(cacheKey) - - if (lookupDs) { - editorOptions.dataSource = lookupDs - editorOptions.valueExpr = colData.lookupDto?.valueExpr?.toLowerCase() || 'key' - editorOptions.displayExpr = colData.lookupDto?.displayExpr?.toLowerCase() || 'name' - editorOptions.searchEnabled = true - editorOptions.showClearButton = true - } - - // Lookup varsa SelectBox kullan (eğer başka bir tip belirtilmediyse) - if (!editorType || editorType === 'dxTextBox') { - editorType = 'dxSelectBox' - } - } - - // Date/DateTime alanları için özel ayarlar - if (colData.sourceDbType === DbTypeEnum.Date) { - editorType = 'dxDateBox' - editorOptions.type = 'date' - editorOptions.dateSerializationFormat = 'yyyy-MM-dd' - editorOptions.displayFormat = 'shortDate' - } else if ( - colData.sourceDbType === DbTypeEnum.DateTime || - colData.sourceDbType === DbTypeEnum.DateTime2 || - colData.sourceDbType === DbTypeEnum.DateTimeOffset - ) { - editorType = 'dxDateBox' - editorOptions.type = 'datetime' - editorOptions.dateSerializationFormat = 'yyyy-MM-ddTHH:mm:ss' - editorOptions.displayFormat = 'shortDateShortTime' - } - - col.formItem = { - ...col.formItem, - colSpan: gridFormItem.colSpan, - } - - if (editorType) { - extCol.editorType = editorType - } - - col.editorOptions = { - ...col.editorOptions, - ...editorOptions, - } - } - - return col - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [gridDto, config]) // getBandedColumns ve lookupDataSource çıkarıldı - sonsuz döngüyü önlemek için - - // Lookup DataSource'ları oluştur ve ref'e ekle - useEffect(() => { - if (!columnsWithLookup || columnsWithLookup.length === 0) return - if (!gridDto) return - lookupDataSourcesRef.current.clear() - columnsWithLookup.forEach((col) => { - const colData = gridDto.columnFormats.find((c) => c.fieldName === col.dataField) - if (!colData) return - - if (colData.lookupDto?.dataSourceType && colData.lookupDto.dataSourceType > 0) { - const cacheKey = `${colData.fieldName}` - const lookupDs = lookupDataSource(null, colData) - lookupDataSourcesRef.current.set(cacheKey, lookupDs) - } - }) - }, [columnsWithLookup, gridDto, lookupDataSource]) - - // Lookup DataSource'ları sayfa açılırken yükle - useEffect(() => { - if (!columnsWithLookup || columnsWithLookup.length === 0) return - if (lookupDataSourcesRef.current.size === 0) return - - const loadLookups = async () => { - const lookupPromises: Array> = [] - - lookupDataSourcesRef.current.forEach((ds, key) => { - if (ds && ds.store && typeof ds.store().load === 'function') { - // DataSource'ın store'unun load metodunu direkt çağır - lookupPromises.push( - ds.store().load().then((items: any[]) => ({ - key, - items: items || [], - })), - ) - } - }) - - if (lookupPromises.length > 0) { - try { - const results = await Promise.all(lookupPromises) - setLookupItemsCache((prev) => { - const newCache = new Map(prev) - results.forEach(({ key, items }) => { - newCache.set(key, items) - }) - return newCache - }) - } catch (error) { - // Hata durumunda sessizce devam et - } - } - } - - loadLookups() - }, [columnsWithLookup]) - - - // Kolonları oluştur - Grid ile aynı şekilde columnData state ile yönet + // Kolonları oluştur - dil değiştiğinde güncelle useEffect(() => { if (!gridDto || !config) return - const cols = getBandedColumns() - setColumnData(cols) + + //butonlar kaldırılarak CardView ekleniyor. + const cols = getBandedColumns()?.filter((c) => c.type !== 'buttons') + + cols?.forEach((col) => { + const eo = col.editorOptions + + // Sadece phoneGlobal formatlı kolonlarda çalış + if (eo?.format === 'phoneGlobal') { + // DevExtreme bazen string tipinde formatter'ı çağırmaz + // Bu yüzden her durumda çalışması için customizeText ekleyeceğiz + col.dataType = 'string' + + const formatter = (value: any) => { + if (!value) return '' + + // string'e dönüştür ve sadece rakamları al + let digits = String(value).replace(/\D/g, '') + + // +90, 0090, 0 gibi ülke kodu veya ön ekleri atla + if (digits.startsWith('90') && digits.length > 10) digits = digits.slice(-10) + if (digits.startsWith('0') && digits.length > 10) digits = digits.slice(-10) + if (digits.length > 10) digits = digits.slice(-10) + + // 🔒 Eğer 10 haneli değilse geçersiz → boş göster + if (digits.length !== 10) return '' + + // (XXX) XXX-XXXX formatında göster + const match = digits.match(/^(\d{3})(\d{3})(\d{4})$/) + return match ? `(${match[1]}) ${match[2]}-${match[3]}` : digits + } + + // 1️⃣ Normal format nesnesi + col.format = { formatter } + + // 2️⃣ CustomizeText fallback — bazı durumlarda zorunlu + col.customizeText = (cellInfo: any) => formatter(cellInfo?.value) + } + }) + + setColumnData(cols as any) }, [gridDto, config]) - // DataSource'u columnData'ya göre oluştur - const [cardViewDataSource, setCardViewDataSource] = useState>() + // DataSource oluştur useEffect(() => { - if (!gridDto || !columnData) return - const ds = createSelectDataSource( + if (!gridDto) return + + const dataSource = createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, - layoutTypes.card, - columnData + layoutTypes.grid, + columnData, ) - setCardViewDataSource(ds) - }, [gridDto, listFormCode, searchParams, columnData]) + + setCardViewDataSource(dataSource) + }, [gridDto, searchParams]) + + useEffect(() => { + if (!columnData) return + + refListFormCode.current = listFormCode + if (!cardViewRef?.current) { + return + } + + const instance = cardViewRef?.current?.instance() + if (instance) { + instance.option('columns', columnData) + instance.option('remoteOperations', { + filtering: true, + sorting: true, + paging: true, + grouping: true, + }) + instance.option('dataSource', cardViewDataSource) + } + }, [columnData]) // extraFilters değişikliğini izlemek için ref const extraFiltersInitialized = useRef(false) @@ -737,120 +518,6 @@ const CardView = (props: CardViewProps) => { } }, [gridDto?.widgets]) - const settingButtonClick = useCallback(() => { - window.open( - ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode), - isPwaMode ? '_self' : '_blank', - ) - }, [listFormCode, isPwaMode]) - - const customizeLookupText = useCallback( - (fieldName: string) => { - return (cellInfo: any) => { - const value = cellInfo.value - if (!value) return '' - - const cacheKey = fieldName - const items = lookupItemsCache.get(cacheKey) - - if (!items || items.length === 0) { - return value - } - - try { - const colData = gridDto?.columnFormats.find((c) => c.fieldName === fieldName) - const valueExpr = colData?.lookupDto?.valueExpr?.toLowerCase() || 'key' - const displayExpr = colData?.lookupDto?.displayExpr?.toLowerCase() || 'name' - - const item = items.find((i: any) => i[valueExpr] === value) - - return item ? item[displayExpr] : value - } catch { - return value - } - } - }, - [gridDto, lookupItemsCache], - ) - - // CardView column render from GridColumnData - const renderColumns = () => { - if (!columnsWithLookup || !Array.isArray(columnsWithLookup) || columnsWithLookup.length === 0) { - return null - } - - return columnsWithLookup - .filter((col) => col.type !== 'buttons' && col.visible !== false) - .map((col) => { - const extCol = col as any - const colData = gridDto?.columnFormats.find((c) => c.fieldName === col.dataField) - - const columnProps: any = { - dataField: col.dataField, - caption: col.caption ? translate('::' + col.caption) : captionize(col.dataField || ''), - dataType: col.dataType, - visible: col.visible, - allowSorting: col.allowSorting, - allowFiltering: col.allowFiltering, - allowHeaderFiltering: col.allowHeaderFiltering, - sortOrder: col.sortOrder, - sortIndex: col.sortIndex, - format: col.format, - alignment: col.alignment, - formItem: col.formItem, - } - - // Lookup varsa customizeText kullan - fieldValueRender çift görünmeye sebep oluyor - if (extCol.isLookup) { - columnProps.customizeText = customizeLookupText(col.dataField!) - } else { - // Lookup değilse editorType ve editorOptions ekle - if (extCol.editorType) { - columnProps.editorType = extCol.editorType - } - if (col.editorOptions) { - columnProps.editorOptions = col.editorOptions - } - - // fieldValueRender - farklı veri tipleri için özel renderlar (lookup değilse) - if (colData?.sourceDbType === DbTypeEnum.Date || col.dataType === 'date') { - columnProps.customizeText = (cellInfo: any) => { - if (!cellInfo.value) return '—' - return new Date(cellInfo.value).toLocaleDateString() - } - } else if ( - colData?.sourceDbType === DbTypeEnum.DateTime || - colData?.sourceDbType === DbTypeEnum.DateTime2 || - colData?.sourceDbType === DbTypeEnum.DateTimeOffset || - col.dataType === 'datetime' - ) { - columnProps.customizeText = (cellInfo: any) => { - if (!cellInfo.value) return '—' - return new Date(cellInfo.value).toLocaleString() - } - } - } - - // Boolean için customizeText kullan (fieldValueRender çift checkbox oluşturuyor) - if (colData?.sourceDbType === DbTypeEnum.Boolean || col.dataType === 'boolean') { - columnProps.customizeText = (cellInfo: any) => { - return cellInfo.value - ? translate('::App.Listforms.ImportManager.Yes') - : translate('::App.Listforms.ImportManager.No') - } - } - - return - }) - } - - // Kolon sayısı değiştiğinde - const onCardsPerRowChanged = useCallback( - (value: number) => { - setCardsPerRow(value) - }, - [], - ) useEffect(() => { if (!gridDto) { return @@ -888,11 +555,6 @@ const CardView = (props: CardViewProps) => { setIsPopupFullScreen(gridDto.gridOptions.editingOptionDto.popup.fullScreen) } - // cardsPerRow başlangıç değeri sadece gridDto'dan alınır - if (gridDto?.gridOptions.layoutDto?.cardLayoutColumn) { - setCardsPerRow(gridDto.gridOptions.layoutDto.cardLayoutColumn) - } - // pageSize başlangıç değeri sadece gridDto'dan alınır if (gridDto?.gridOptions.pageSize) { setPageSize(gridDto.gridOptions.pageSize) @@ -972,10 +634,22 @@ const CardView = (props: CardViewProps) => { } return items - }, [ - gridDto, - cardsPerRow - ]) + }, [gridDto, cardsPerRow]) + + // Kolon sayısı değiştiğinde hem state'e hem store'a kaydet + const onCardsPerRowChanged = useCallback( + (value: number) => { + setCardsPerRow(value) + // admin.lists.states'e kaydet + setAdminStates && + setAdminStates({ + listFormCode, + layout: 'card', + cardLayoutColumn: value, + }) + }, + [listFormCode, setAdminStates], + ) // Paging ayarları const pagingConfig = useMemo( @@ -1039,13 +713,12 @@ const CardView = (props: CardViewProps) => { )} - {gridDto && columnsWithLookup && Array.isArray(columnsWithLookup) && columnsWithLookup.length > 0 && cardViewDataSource && ( -
+ {gridDto && cardViewDataSource && ( +
{ ? `calc(100vh - ${170 + widgetGroupHeight}px)` : undefined } - remoteOperations={true} - onSelectionChanged={onSelectionChanged as any} - onInitNewCard={onInitNewCard as any} - onCardInserting={onCardInserting as any} - onCardUpdating={onCardUpdating as any} - onEditingStart={onEditingStart as any} - onDataErrorOccurred={onDataErrorOccurred as any} + onSelectionChanged={onSelectionChanged} + onInitNewCard={onInitNewRow} + onCardInserting={onRowInserting} + onCardUpdating={onRowUpdating} + onEditingStart={onEditingStart} onEditCanceled={() => { setMode('view') setIsPopupFullScreen(false) @@ -1072,32 +743,20 @@ const CardView = (props: CardViewProps) => { setMode('view') setIsPopupFullScreen(false) // Küçük bir gecikme ile reload - server transaction commit bekle - setTimeout(() => { - refreshData() - props.refreshData?.() - }, 100) + refreshData() + props.refreshData?.() }} onCardUpdated={() => { setMode('view') setIsPopupFullScreen(false) - // Küçük bir gecikme ile reload - server transaction commit bekle - setTimeout(() => { - refreshData() - props.refreshData?.() - }, 100) + refreshData() + props.refreshData?.() }} onCardRemoved={() => { - // Küçük bir gecikme ile reload - server transaction commit bekle - setTimeout(() => { - refreshData() - props.refreshData?.() - }, 100) + refreshData() + props.refreshData?.() }} - // onOptionChanged kaldırıldı, pageSize sadece state ve useEffect ile kontrol ediliyor > - {/* Toolbar */} - - {/* Selection */} { const cardView = cardViewRef.current?.instance() if (cardView) { // Form validasyonu yap - const editForm = (cardView as any).getController?.('validating')?.validate?.() - + const editForm = (cardView as any) + .getController?.('validating') + ?.validate?.() + // Eğer validate fonksiyonu yoksa direkt kaydet if (!editForm) { cardView.saveEditData() return } - + // Validasyon hatası varsa kaydetme if (editForm && !editForm.brokenRules?.length) { cardView.saveEditData() @@ -1238,8 +899,21 @@ const CardView = (props: CardViewProps) => { ], }} form={{ - colCount: gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2, - showValidationSummary: true, + colCount: 1, + onFieldDataChanged: (e) => { + if (e.dataField) { + const formItem = gridDto.gridOptions.editingFormDto + .flatMap((group) => group.items || []) + .find((i) => i.dataField === e.dataField) + if (formItem?.editorScript) { + try { + eval(formItem.editorScript) + } catch (err) { + console.error('Script exec error', err) + } + } + } + }, items: gridDto.gridOptions.editingFormDto?.length > 0 ? (() => { @@ -1247,16 +921,53 @@ const CardView = (props: CardViewProps) => { .slice() .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) + // Tabbed item'ları grupla const tabbedItems = sortedFormDto.filter( (e: any) => e.itemType === 'tabbed', ) const result: any[] = [] + // Helper function: item mapper const mapFormItem = (i: EditingFormItemDto) => { - let editorOptions: any = {} + let editorOptions: EditorOptionsWithButtons = {} try { - if (i.editorOptions) { - editorOptions = JSON.parse(i.editorOptions) + editorOptions = i.editorOptions && JSON.parse(i.editorOptions) + + if (editorOptions?.buttons) { + editorOptions.buttons = (editorOptions?.buttons || []).map( + (btn: any) => { + if ( + btn?.options?.onClick && + typeof btn.options.onClick === 'string' + ) { + btn.options.onClick = eval(`(${btn.options.onClick})`) + } + return btn + }, + ) + } + + const rawFilter = searchParams?.get('filter') + if (rawFilter) { + const parsed = JSON.parse(rawFilter) + const filters = extractSearchParamsFields(parsed) + + const hasFilter = filters.some( + ([field, op, val]) => field === i.dataField, + ) + + if (hasFilter) { + const existsInExtra = extraFilters.some( + (f) => f.fieldName === i.dataField && !!f.value, + ) + + if (!existsInExtra) { + editorOptions = { + ...editorOptions, + readOnly: true, + } + } + } } } catch {} @@ -1301,88 +1012,97 @@ const CardView = (props: CardViewProps) => { } } - // EditorType belirleme - let editorType: any = i.editorType2 || i.editorType - if (i.editorType2 === PlatformEditorTypes.dxGridBox) { - editorType = 'dxDropDownBox' - } else if (i.editorType2) { - editorType = i.editorType2 - } - - // Lookup DataSource oluştur - if ( - listFormField?.lookupDto && - listFormField.lookupDto.dataSourceType > 0 - ) { - const cacheKey = `${listFormField.fieldName}` - let lookupDs = lookupDataSourcesRef.current.get(cacheKey) - - if (!lookupDs) { - lookupDs = lookupDataSource(null, listFormField) - lookupDataSourcesRef.current.set(cacheKey, lookupDs) - } - - editorOptions.dataSource = lookupDs - editorOptions.valueExpr = - listFormField.lookupDto.valueExpr?.toLowerCase() || 'key' - editorOptions.displayExpr = - listFormField.lookupDto.displayExpr?.toLowerCase() || 'name' - editorOptions.searchEnabled = true - editorOptions.showClearButton = true - - if (!editorType || editorType === 'dxTextBox') { - editorType = 'dxSelectBox' - } - } - - const item: any = { + const item: SimpleItemWithColData = { + canRead: listFormField?.canRead ?? false, + canUpdate: listFormField?.canUpdate ?? false, + canCreate: listFormField?.canCreate ?? false, + canExport: listFormField?.canExport ?? false, dataField: i.dataField, - editorType: editorType, + name: i.dataField, + editorType2: i.editorType2, + editorType: + i.editorType2 == PlatformEditorTypes.dxGridBox + ? 'dxDropDownBox' + : i.editorType2, colSpan: i.colSpan, isRequired: i.isRequired, editorOptions, - } - - // Required field için validasyon kuralı ekle - if (i.isRequired) { - item.validationRules = [ - { - type: 'required', - message: `${i.dataField.split(':')[1] || i.dataField} zorunludur`, - }, - ] + editorScript: i.editorScript, } if (i.dataField.indexOf(':') >= 0) { item.label = { text: captionize(i.dataField.split(':')[1]) } } + if ( + (mode == 'edit' && !item.canUpdate) || + (mode == 'new' && !item.canCreate) + ) { + item.editorOptions = { + ...item.editorOptions, + readOnly: true, + } + } + return item } sortedFormDto.forEach((e: any) => { if (e.itemType !== 'tabbed') { + // Backend'den gelen colCount ve colSpan değerlerini kullan + const effectiveColCount = e.colCount || 1 + const effectiveColSpan = e.colSpan || 1 + result.push({ itemType: e.itemType, - colCount: e.colCount || 1, - colSpan: e.colSpan || 1, - caption: e.caption, + colCount: effectiveColCount, + colSpan: effectiveColSpan, + caption: e.caption, // Group'larda caption olmalı items: e.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) - .map(mapFormItem), + .map(mapFormItem) + .filter((a: any) => { + if (mode === 'view') { + return a.canRead + } else if (mode === 'new') { + return a.canCreate || a.canRead + } else if (mode === 'edit') { + return a.canUpdate || a.canRead + } else { + return false + } + }), }) } else if (tabbedItems.length > 0 && e === tabbedItems[0]) { + // Tabbed için caption OLMAMALI - sadece tabs array içinde title kullan result.push({ itemType: 'tabbed', colCount: 1, colSpan: 1, - tabs: tabbedItems.map((tabbedItem: any) => ({ - title: tabbedItem.caption, - colCount: tabbedItem.colCount || 1, - items: tabbedItem.items - ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) - .map(mapFormItem), - })), + // caption kullanma! Tabs içindeki title'lar yeterli + tabs: tabbedItems.map((tabbedItem: any) => { + // Backend'den gelen colCount ve colSpan değerlerini kullan + const effectiveColCount = tabbedItem.colCount || 1 + + return { + title: tabbedItem.caption, // Her tab'ın title'ı + colCount: effectiveColCount, + items: tabbedItem.items + ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) + .map(mapFormItem) + .filter((a: any) => { + if (mode === 'view') { + return a.canRead + } else if (mode === 'new') { + return a.canCreate || a.canRead + } else if (mode === 'edit') { + return a.canUpdate || a.canRead + } else { + return false + } + }), + } + }), }) } }) @@ -1392,9 +1112,47 @@ const CardView = (props: CardViewProps) => { : undefined, }} /> +