Grid, Pivot ve Tree Exporting

This commit is contained in:
Sedat ÖZTÜRK 2026-02-05 11:32:04 +03:00
parent 809bfb7129
commit 12bac78cb8
6 changed files with 533 additions and 510 deletions

View file

@ -1,5 +1,5 @@
{
"commit": "270e50e0",
"commit": "809bfb71",
"releases": [
{
"version": "1.0.40",

View file

@ -1,5 +1,5 @@
import { Container } from '@/components/shared'
import { Button, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
import { LanguageInfo } from '@/proxy/config/models'
import { SelectBoxOption } from '@/types/shared'
@ -63,7 +63,6 @@ function FormTabDetails(
}, [languages])
return (
<Container className="grid xl:grid-cols-2">
<Formik
initialValues={listFormValues}
validationSchema={schema}
@ -74,6 +73,8 @@ function FormTabDetails(
{({ touched, errors, isSubmitting, values }) => (
<Form>
<FormContainer size="sm">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Card className="my-2" header="General">
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsCultureName')}
invalid={errors.cultureName && touched.cultureName}
@ -140,6 +141,7 @@ function FormTabDetails(
component={Input}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsName')}
invalid={errors.name && touched.name}
@ -153,6 +155,7 @@ function FormTabDetails(
component={Input}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
invalid={errors.description && touched.description}
@ -166,46 +169,6 @@ function FormTabDetails(
component={Input}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.IsSub')}
invalid={errors.isSubForm && touched.isSubForm}
errorMessage={errors.isSubForm}
>
<Field
name="isSubForm"
placeholder={translate('::ListForms.ListFormEdit.IsSub')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.SubFormsListFormType')}
invalid={errors.subFormsListFormType && touched.subFormsListFormType}
errorMessage={errors.subFormsListFormType}
>
<Field
type="text"
autoComplete="off"
name="subFormsListFormType"
placeholder={translate('::ListForms.ListFormEdit.SubFormsListFormType')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={listFormTypeOptions}
value={listFormTypeOptions?.filter(
(option) => option.value === values.subFormsListFormType,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
props.onFormTypeChange(option?.value || 'List')
}}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.ShowNote')}
@ -218,7 +181,8 @@ function FormTabDetails(
component={Checkbox}
/>
</FormItem>
</Card>
<Card className="my-2" header="Layout & Popup">
<div className="flex gap-2">
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsWidth')}
@ -260,6 +224,45 @@ function FormTabDetails(
/>
</FormItem>
</div>
<FormItem
label={translate('::ListForms.ListFormEdit.IsSub')}
invalid={errors.isSubForm && touched.isSubForm}
errorMessage={errors.isSubForm}
>
<Field
name="isSubForm"
placeholder={translate('::ListForms.ListFormEdit.IsSub')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.SubFormsListFormType')}
invalid={errors.subFormsListFormType && touched.subFormsListFormType}
errorMessage={errors.subFormsListFormType}
>
<Field
type="text"
autoComplete="off"
name="subFormsListFormType"
placeholder={translate('::ListForms.ListFormEdit.SubFormsListFormType')}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={listFormTypeOptions}
value={listFormTypeOptions?.filter(
(option) => option.value === values.subFormsListFormType,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
/>
)}
</Field>
</FormItem>
{values.listFormType === 'List' && (
<>
@ -290,8 +293,6 @@ function FormTabDetails(
)}
</Field>
</FormItem>
</>
)}
<div className="flex gap-2">
<FormItem
@ -303,7 +304,9 @@ function FormTabDetails(
className="w-20"
autoComplete="off"
name="layoutDto.grid"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout',
)}
component={Checkbox}
/>
</FormItem>
@ -317,7 +320,9 @@ function FormTabDetails(
className="w-20"
autoComplete="off"
name="layoutDto.pivot"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout',
)}
component={Checkbox}
/>
</FormItem>
@ -331,7 +336,9 @@ function FormTabDetails(
className="w-20"
autoComplete="off"
name="layoutDto.chart"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.ChartLayout')}
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.ChartLayout',
)}
component={Checkbox}
/>
</FormItem>
@ -345,7 +352,9 @@ function FormTabDetails(
className="w-20"
autoComplete="off"
name="layoutDto.tree"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.TreeLayout')}
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.TreeLayout',
)}
component={Checkbox}
/>
</FormItem>
@ -359,13 +368,17 @@ function FormTabDetails(
className="w-20"
autoComplete="off"
name="layoutDto.gantt"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout')}
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout',
)}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.SchedulerLayout')}
label={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.SchedulerLayout',
)}
invalid={errors.layoutDto?.scheduler && touched.layoutDto?.scheduler}
errorMessage={errors.layoutDto?.scheduler}
>
@ -380,6 +393,8 @@ function FormTabDetails(
/>
</FormItem>
</div>
</>
)}
<FormItem
label="Role"
@ -418,6 +433,8 @@ function FormTabDetails(
)}
</Field>
</FormItem>
</Card>
</div>
<Button block variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
@ -426,7 +443,6 @@ function FormTabDetails(
</Form>
)}
</Formik>
</Container>
)
}

View file

@ -29,7 +29,6 @@ import DataGrid, {
GroupItem as GroupItemDx,
GroupPanel,
HeaderFilter,
IStateStoringProps,
LoadPanel,
Pager,
Paging,
@ -74,7 +73,6 @@ import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { useListFormColumns } from './useListFormColumns'
import { Loading } from '@/components/shared'
import { useStoreState } from '@/store'
import { locale, loadMessages } from 'devextreme/localization'
interface GridProps {
listFormCode: string
@ -108,14 +106,6 @@ const Grid = (props: GridProps) => {
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const preloadExportLibs = () => {
import('exceljs')
import('file-saver')
import('devextreme/excel_exporter')
import('jspdf')
import('devextreme/pdf_exporter')
}
type EditorOptionsWithButtons = {
buttons?: any[]
} & Record<string, any>
@ -985,9 +975,9 @@ const Grid = (props: GridProps) => {
if (!grid) return
try {
if (e.format === 'xlsx' || e.format === 'csv') {
// exceljs + file-saver + devextreme excel exporter => ihtiyaç anında yükle
const [{ Workbook }, { saveAs }, { exportDataGrid: exportDataExcel }] = await Promise.all([
if (e.format === 'xlsx') {
// Sadece Excel için gerekli kütüphaneleri yükle
const [{ Workbook }, { saveAs }, { exportDataGrid }] = await Promise.all([
import('exceljs'),
import('file-saver'),
import('devextreme/excel_exporter'),
@ -996,25 +986,45 @@ const Grid = (props: GridProps) => {
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
await exportDataExcel({
await exportDataGrid({
component: grid as any,
worksheet,
autoFilterEnabled: true,
})
if (e.format === 'xlsx') {
const buffer = await workbook.xlsx.writeBuffer()
saveAs(
new Blob([buffer], { type: 'application/octet-stream' }),
`${listFormCode}_export.xlsx`,
)
} else {
} else if (e.format === 'csv') {
// Sadece CSV için gerekli kütüphaneleri yükle (exceljs CSV desteği için)
const [{ Workbook }, { saveAs }] = await Promise.all([
import('exceljs'),
import('file-saver'),
])
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
// CSV için basit data export
const dataSource = grid.getDataSource()
const items = dataSource?.items() || []
const columns = grid.getVisibleColumns().filter((c: any) => c.dataField)
// Header ekle
worksheet.addRow(columns.map((c: any) => c.caption || c.dataField))
// Data ekle
items.forEach((item: any) => {
worksheet.addRow(columns.map((c: any) => item[c.dataField!]))
})
const buffer = await workbook.csv.writeBuffer()
saveAs(
new Blob([buffer], { type: 'application/octet-stream' }),
new Blob([buffer], { type: 'text/csv' }),
`${listFormCode}_export.csv`,
)
}
} else if (e.format === 'pdf') {
// jspdf + devextreme pdf exporter => ihtiyaç anında yükle
const [jspdfMod, { exportDataGrid: exportDataPdf }] = await Promise.all([
@ -1151,7 +1161,7 @@ const Grid = (props: GridProps) => {
<Export
enabled={gridDto.gridOptions.exportDto?.enabled}
allowExportSelectedData={gridDto.gridOptions.exportDto?.allowExportSelectedData}
formats={['pdf', 'xlsx', 'csv']}
formats={['pdf', 'xlsx']}
/>
<Editing
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}

View file

@ -6,12 +6,7 @@ import {
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Chart, {
ChartRef,
CommonSeriesSettings,
Size,
Tooltip,
} from 'devextreme-react/chart'
import Chart, { ChartRef, CommonSeriesSettings, Size, Tooltip } from 'devextreme-react/chart'
import PivotGrid, {
Export,
FieldChooser,
@ -22,10 +17,9 @@ import PivotGrid, {
PivotGridTypes,
Scrolling,
Search,
StateStoring,
} from 'devextreme-react/pivot-grid'
import CustomStore from 'devextreme/data/custom_store'
import PivotGridDataSource, { Field } from 'devextreme/ui/pivot_grid/data_source'
import { Field } from 'devextreme/ui/pivot_grid/data_source'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { GridColumnData } from './GridColumnData'
@ -93,7 +87,8 @@ const Pivot = (props: PivotProps) => {
gridRef,
})
const onCellPrepared = useCallback((e: any) => {
const onCellPrepared = useCallback(
(e: any) => {
const columnFormats = gridDto?.columnFormats
if (!columnFormats) {
return
@ -118,14 +113,19 @@ const Pivot = (props: PivotProps) => {
}
// css inline style var ise uygula
if (colStyle.cssStyles) {
e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles)
e.cellElement.attr(
'style',
e.cellElement.attr('style') + ';' + colStyle.cssStyles,
)
}
}
}
}
}
}
}, [gridDto])
},
[gridDto],
)
const clearPivotFilters = useCallback(() => {
const grid = gridRef.current?.instance()
@ -191,7 +191,8 @@ const Pivot = (props: PivotProps) => {
}
}, [listFormCode, storageKey, clearPivotFilters, translate])
const onExporting = useCallback(async (e: PivotGridTypes.ExportingEvent) => {
const onExporting = useCallback(
async (e: PivotGridTypes.ExportingEvent) => {
e.cancel = true
const pivot = gridRef?.current?.instance()
@ -227,7 +228,9 @@ const Pivot = (props: PivotProps) => {
{ placement: 'top-end' },
)
}
}, [listFormCode, translate])
},
[listFormCode, translate],
)
// StateStoring fonksiyonlarını ref'e kaydet
const customSaveState = useCallback(
@ -244,8 +247,7 @@ const Pivot = (props: PivotProps) => {
[listFormCode, storageKey],
)
const customLoadState = useCallback(
() => {
const customLoadState = useCallback(() => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
@ -257,9 +259,7 @@ const Pivot = (props: PivotProps) => {
}
return null
})
},
[listFormCode, storageKey],
)
}, [listFormCode, storageKey])
useEffect(() => {
refListFormCode.current = listFormCode
@ -373,14 +373,16 @@ const Pivot = (props: PivotProps) => {
const instance = gridRef?.current?.instance()
if (instance) {
customLoadState().then((state) => {
customLoadState()
.then((state) => {
if (state) {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(state)
}
}
}).catch((err) => {
})
.catch((err) => {
console.error('Pivot state load error:', err)
})
}
@ -416,7 +418,6 @@ const Pivot = (props: PivotProps) => {
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 ">
<div className="flex justify-end items-center">
<div className="relative pb-1 flex gap-1 border-b-1">
<Button
size="xs"
variant={'default'}
@ -437,14 +438,16 @@ const Pivot = (props: PivotProps) => {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
const currentState = ds.state()
customSaveState(currentState).then(() => {
customSaveState(currentState)
.then(() => {
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{ placement: 'top-end' },
)
}).catch(() => {
})
.catch(() => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
@ -541,7 +544,9 @@ const Pivot = (props: PivotProps) => {
height={500}
/>
<LoadPanel
enabled={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled as boolean | undefined}
enabled={
gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled as boolean | undefined
}
text={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
/>
<Scrolling mode={gridDto.gridOptions.pagerOptionDto.scrollingMode} />

View file

@ -101,14 +101,6 @@ const Tree = (props: TreeProps) => {
const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([])
const config = useStoreState((state) => state.abpConfig.config)
const preloadExportLibs = () => {
import('exceljs')
import('file-saver')
import('devextreme/excel_exporter')
import('jspdf')
import('devextreme/pdf_exporter')
}
type EditorOptionsWithButtons = {
buttons?: any[]
} & Record<string, any>

View file

@ -275,50 +275,50 @@ const useListFormCustomDataSource = ({
return null
}
},
// totalCount: async (loadOptions) => {
// const parameters = getLoadOptions(loadOptions, {
// listFormCode,
// filter: '',
// createDeleteQuery: searchParams?.get('createDeleteQuery'),
// group: '',
// })
totalCount: async (loadOptions) => {
const parameters = getLoadOptions(loadOptions, {
listFormCode,
filter: '',
createDeleteQuery: searchParams?.get('createDeleteQuery'),
group: '',
})
// // 1. Default filter'ı al
// const defaultFilter = searchParams?.get('filter')
// ? JSON.parse(searchParams.get('filter')!)
// : null
// 1. Default filter'ı al
const defaultFilter = searchParams?.get('filter')
? JSON.parse(searchParams.get('filter')!)
: null
// let combinedFilter: any = parameters.filter
let combinedFilter: any = parameters.filter
// // 2. Eğer hem default hem de grid filter varsa merge et
// if (defaultFilter && combinedFilter) {
// combinedFilter = [defaultFilter, 'and', combinedFilter]
// } else if (defaultFilter) {
// combinedFilter = defaultFilter
// }
// 2. Eğer hem default hem de grid filter varsa merge et
if (defaultFilter && combinedFilter) {
combinedFilter = [defaultFilter, 'and', combinedFilter]
} else if (defaultFilter) {
combinedFilter = defaultFilter
}
// if (combinedFilter && combinedFilter.length > 0) {
// parameters.filter = JSON.stringify(combinedFilter)
// } else {
// delete parameters.filter // hiç göndermesin
// }
if (combinedFilter && combinedFilter.length > 0) {
parameters.filter = JSON.stringify(combinedFilter)
} else {
delete parameters.filter // hiç göndermesin
}
// try {
// const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
// return response.data.totalCount
// } catch (error: any) {
// // toast.push(
// // <Notification type="danger" duration={2000}>
// // TotalCount error
// // {error.toString()}
// // </Notification>,
// // {
// // placement: 'top-end',
// // },
// // )
// return null
// }
try {
const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
return response.data.totalCount
} catch (error: any) {
// toast.push(
// <Notification type="danger" duration={2000}>
// TotalCount error
// {error.toString()}
// </Notification>,
// {
// placement: 'top-end',
// },
// )
return null
}
},
byKey: async (key) => {
const parameters = getLoadOptions(
{ key },