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": [ "releases": [
{ {
"version": "1.0.40", "version": "1.0.40",

View file

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

View file

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

View file

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

View file

@ -101,14 +101,6 @@ const Tree = (props: TreeProps) => {
const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([]) const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([])
const config = useStoreState((state) => state.abpConfig.config) 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 = { type EditorOptionsWithButtons = {
buttons?: any[] buttons?: any[]
} & Record<string, any> } & Record<string, any>

View file

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