F: Add JSON export option
This commit is contained in:
parent
f7118bc96a
commit
f27d9df8d3
|
@ -0,0 +1,83 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import fileDownload from 'js-file-download';
|
||||||
|
|
||||||
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
import { convertToCSV, convertToJSON } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { Dropdown, DropdownButton, useDropdown } from '../dropdown';
|
||||||
|
import { IconCSV, IconDownload, IconJSON } from '../icons';
|
||||||
|
import { cn } from '../utils';
|
||||||
|
|
||||||
|
import { MiniButton } from './mini-button';
|
||||||
|
|
||||||
|
interface ExportDropdownProps<T extends object = object> {
|
||||||
|
/** Disabled state */
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/** Data to export (can be readonly or mutable array of objects) */
|
||||||
|
data: readonly Readonly<T>[];
|
||||||
|
|
||||||
|
/** Optional filename (without extension) */
|
||||||
|
|
||||||
|
filename?: string;
|
||||||
|
/** Optional button className */
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportDropdown<T extends object = object>({
|
||||||
|
disabled,
|
||||||
|
data,
|
||||||
|
filename = 'export',
|
||||||
|
className
|
||||||
|
}: ExportDropdownProps<T>) {
|
||||||
|
const { ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
||||||
|
|
||||||
|
function handleExport(format: 'csv' | 'json') {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
toast.error(infoMsg.noDataToExport);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (format === 'csv') {
|
||||||
|
const blob = convertToCSV(data);
|
||||||
|
fileDownload(blob, `${filename}.csv`, 'text/csv;charset=utf-8;');
|
||||||
|
} else {
|
||||||
|
const blob = convertToJSON(data);
|
||||||
|
fileDownload(blob, `${filename}.json`, 'application/json;charset=utf-8;');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative inline-block', className)} tabIndex={0} onBlur={handleBlur}>
|
||||||
|
<MiniButton
|
||||||
|
title='Экспортировать данные'
|
||||||
|
hideTitle={isOpen}
|
||||||
|
className='text-muted-foreground enabled:hover:text-primary'
|
||||||
|
icon={<IconDownload size='1.25rem' />}
|
||||||
|
onClick={toggle}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Dropdown ref={ref} isOpen={isOpen} stretchLeft margin='mt-1' className='z-tooltip'>
|
||||||
|
<DropdownButton
|
||||||
|
icon={<IconCSV size='1rem' className='mr-1 icon-green' />}
|
||||||
|
text='CSV'
|
||||||
|
onClick={() => handleExport('csv')}
|
||||||
|
className='w-full justify-start'
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
icon={<IconJSON size='1rem' className='mr-1 icon-green' />}
|
||||||
|
text='JSON'
|
||||||
|
onClick={() => handleExport('json')}
|
||||||
|
className='w-full justify-start'
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -61,6 +61,7 @@ export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
||||||
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
|
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
|
||||||
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
|
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
|
||||||
export { PiFileCsv as IconCSV } from 'react-icons/pi';
|
export { PiFileCsv as IconCSV } from 'react-icons/pi';
|
||||||
|
export { BsFiletypeJson as IconJSON } from 'react-icons/bs';
|
||||||
|
|
||||||
// ==== User status =======
|
// ==== User status =======
|
||||||
export { LuCircleUserRound as IconUser } from 'react-icons/lu';
|
export { LuCircleUserRound as IconUser } from 'react-icons/lu';
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { ExportDropdown } from '@/components/control/export-dropdown';
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
|
||||||
import { IconCSV } from '@/components/icons';
|
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { infoMsg } from '@/utils/labels';
|
|
||||||
import { convertToCSV } from '@/utils/utils';
|
|
||||||
|
|
||||||
import { useApplyLibraryFilter } from '../../backend/use-apply-library-filter';
|
import { useApplyLibraryFilter } from '../../backend/use-apply-library-filter';
|
||||||
import { useLibrarySuspense } from '../../backend/use-library';
|
import { useLibrarySuspense } from '../../backend/use-library';
|
||||||
|
@ -38,28 +32,14 @@ export function LibraryPage() {
|
||||||
}).then(() => setLocation(newLocation));
|
}).then(() => setLocation(newLocation));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDownloadCSV() {
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
toast.error(infoMsg.noDataToExport);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const blob = convertToCSV(filtered);
|
|
||||||
try {
|
|
||||||
fileDownload(blob, 'library.csv', 'text/csv;charset=utf-8;');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
||||||
<div className='relative flex'>
|
<div className='relative flex'>
|
||||||
<MiniButton
|
<ExportDropdown
|
||||||
title='Выгрузить в формате CSV'
|
data={filtered}
|
||||||
|
filename='library'
|
||||||
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
||||||
icon={<IconCSV size='1.25rem' className='text-muted-foreground hover:text-constructive' />}
|
|
||||||
onClick={handleDownloadCSV}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewSideLocation
|
<ViewSideLocation
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { ExportDropdown } from '@/components/control/export-dropdown';
|
||||||
import { type RowSelectionState } from '@/components/data-table';
|
import { type RowSelectionState } from '@/components/data-table';
|
||||||
import { IconCSV } from '@/components/icons';
|
|
||||||
import { SearchBar } from '@/components/input';
|
import { SearchBar } from '@/components/input';
|
||||||
import { useFitHeight } from '@/stores/app-layout';
|
import { useFitHeight } from '@/stores/app-layout';
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { withPreventDefault } from '@/utils/utils';
|
||||||
import { convertToCSV, withPreventDefault } from '@/utils/utils';
|
|
||||||
|
|
||||||
import { CstType } from '../../../backend/types';
|
import { CstType } from '../../../backend/types';
|
||||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||||
|
@ -47,19 +43,6 @@ export function EditorRSList() {
|
||||||
filtered.map((cst, index) => [String(index), selected.includes(cst.id)])
|
filtered.map((cst, index) => [String(index), selected.includes(cst.id)])
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleDownloadCSV() {
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
toast.error(infoMsg.noDataToExport);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const blob = convertToCSV(filtered);
|
|
||||||
try {
|
|
||||||
fileDownload(blob, `${schema.alias}.csv`, 'text/csv;charset=utf-8;');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||||
const newSelection: number[] = [];
|
const newSelection: number[] = [];
|
||||||
|
@ -140,11 +123,11 @@ export function EditorRSList() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MiniButton
|
<ExportDropdown
|
||||||
|
data={filtered}
|
||||||
|
filename={schema.alias}
|
||||||
className='absolute z-pop right-4 hidden sm:block top-18'
|
className='absolute z-pop right-4 hidden sm:block top-18'
|
||||||
title='Выгрузить в формате CSV'
|
disabled={filtered.length === 0}
|
||||||
icon={<IconCSV size='1.25rem' className='text-constructive' />}
|
|
||||||
onClick={handleDownloadCSV}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRSList
|
<TableRSList
|
||||||
|
|
|
@ -156,6 +156,14 @@ export function convertToCSV(targetObj: readonly object[]): Blob {
|
||||||
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert object or array to JSON Blob.
|
||||||
|
*/
|
||||||
|
export function convertToJSON(targetObj: unknown): Blob {
|
||||||
|
const jsonString = JSON.stringify(targetObj, null, 2);
|
||||||
|
return new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a QR code for the current page.
|
* Generates a QR code for the current page.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue
Block a user