F: Add JSON export option

This commit is contained in:
Ivan 2025-07-19 10:59:56 +03:00
parent f7118bc96a
commit f27d9df8d3
5 changed files with 102 additions and 47 deletions

View File

@ -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>
);
}

View File

@ -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';

View File

@ -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

View File

@ -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

View File

@ -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.
*/