From f27d9df8d35c86fe9b461e4b30a58c8858d680b1 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:59:56 +0300 Subject: [PATCH] F: Add JSON export option --- .../components/control/export-dropdown.tsx | 83 +++++++++++++++++++ rsconcept/frontend/src/components/icons.tsx | 1 + .../pages/library-page/library-page.tsx | 28 +------ .../editor-rslist/editor-rslist.tsx | 29 ++----- rsconcept/frontend/src/utils/utils.ts | 8 ++ 5 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 rsconcept/frontend/src/components/control/export-dropdown.tsx diff --git a/rsconcept/frontend/src/components/control/export-dropdown.tsx b/rsconcept/frontend/src/components/control/export-dropdown.tsx new file mode 100644 index 00000000..b8177c60 --- /dev/null +++ b/rsconcept/frontend/src/components/control/export-dropdown.tsx @@ -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 { + /** Disabled state */ + disabled?: boolean; + + /** Data to export (can be readonly or mutable array of objects) */ + data: readonly Readonly[]; + + /** Optional filename (without extension) */ + + filename?: string; + /** Optional button className */ + + className?: string; +} + +export function ExportDropdown({ + disabled, + data, + filename = 'export', + className +}: ExportDropdownProps) { + 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 ( +
+ } + onClick={toggle} + disabled={disabled} + /> + + } + text='CSV' + onClick={() => handleExport('csv')} + className='w-full justify-start' + /> + } + text='JSON' + onClick={() => handleExport('json')} + className='w-full justify-start' + /> + +
+ ); +} diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx index fc87fd99..2df88406 100644 --- a/rsconcept/frontend/src/components/icons.tsx +++ b/rsconcept/frontend/src/components/icons.tsx @@ -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'; diff --git a/rsconcept/frontend/src/features/library/pages/library-page/library-page.tsx b/rsconcept/frontend/src/features/library/pages/library-page/library-page.tsx index 063bdabb..dee9189b 100644 --- a/rsconcept/frontend/src/features/library/pages/library-page/library-page.tsx +++ b/rsconcept/frontend/src/features/library/pages/library-page/library-page.tsx @@ -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 ( <>
- } - onClick={handleDownloadCSV} /> [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) { const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater; const newSelection: number[] = []; @@ -140,11 +123,11 @@ export function EditorRSList() { />
- } - onClick={handleDownloadCSV} + disabled={filtered.length === 0} />