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 { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
|
||||
export { PiFileCsv as IconCSV } from 'react-icons/pi';
|
||||
export { BsFiletypeJson as IconJSON } from 'react-icons/bs';
|
||||
|
||||
// ==== User status =======
|
||||
export { LuCircleUserRound as IconUser } from 'react-icons/lu';
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import fileDownload from 'js-file-download';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconCSV } from '@/components/icons';
|
||||
import { ExportDropdown } from '@/components/control/export-dropdown';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
import { convertToCSV } from '@/utils/utils';
|
||||
|
||||
import { useApplyLibraryFilter } from '../../backend/use-apply-library-filter';
|
||||
import { useLibrarySuspense } from '../../backend/use-library';
|
||||
|
@ -38,28 +32,14 @@ export function LibraryPage() {
|
|||
}).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 (
|
||||
<>
|
||||
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
||||
<div className='relative flex'>
|
||||
<MiniButton
|
||||
title='Выгрузить в формате CSV'
|
||||
<ExportDropdown
|
||||
data={filtered}
|
||||
filename='library'
|
||||
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
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
'use client';
|
||||
|
||||
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 { IconCSV } from '@/components/icons';
|
||||
import { SearchBar } from '@/components/input';
|
||||
import { useFitHeight } from '@/stores/app-layout';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
import { convertToCSV, withPreventDefault } from '@/utils/utils';
|
||||
import { withPreventDefault } from '@/utils/utils';
|
||||
|
||||
import { CstType } from '../../../backend/types';
|
||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||
|
@ -47,19 +43,6 @@ export function EditorRSList() {
|
|||
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>) {
|
||||
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
const newSelection: number[] = [];
|
||||
|
@ -140,11 +123,11 @@ export function EditorRSList() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<MiniButton
|
||||
<ExportDropdown
|
||||
data={filtered}
|
||||
filename={schema.alias}
|
||||
className='absolute z-pop right-4 hidden sm:block top-18'
|
||||
title='Выгрузить в формате CSV'
|
||||
icon={<IconCSV size='1.25rem' className='text-constructive' />}
|
||||
onClick={handleDownloadCSV}
|
||||
disabled={filtered.length === 0}
|
||||
/>
|
||||
|
||||
<TableRSList
|
||||
|
|
|
@ -156,6 +156,14 @@ export function convertToCSV(targetObj: readonly object[]): Blob {
|
|||
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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue
Block a user