diff --git a/rsconcept/frontend/src/components/Icons.tsx b/rsconcept/frontend/src/components/Icons.tsx index f391e9df..bce36299 100644 --- a/rsconcept/frontend/src/components/Icons.tsx +++ b/rsconcept/frontend/src/components/Icons.tsx @@ -51,6 +51,7 @@ export { BiFirstPage as IconPageFirst } from 'react-icons/bi'; 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'; // ==== User status ======= export { LuUserCircle2 as IconUser } from 'react-icons/lu'; @@ -69,7 +70,7 @@ export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { TbHexagons as IconOSS } from 'react-icons/tb'; export { TbHexagon as IconRSForm } from 'react-icons/tb'; export { TbTopologyRing as IconConsolidation } from 'react-icons/tb'; -export { GrInherit as IconChild } from 'react-icons/gr'; +export { LiaCloneSolid as IconChild } from 'react-icons/lia'; export { RiParentLine as IconParent } from 'react-icons/ri'; export { BiSpa as IconPredecessor } from 'react-icons/bi'; export { LuArchive as IconArchive } from 'react-icons/lu'; diff --git a/rsconcept/frontend/src/components/ui/IconValue.tsx b/rsconcept/frontend/src/components/ui/IconValue.tsx index ab1cd8c6..f7898bf9 100644 --- a/rsconcept/frontend/src/components/ui/IconValue.tsx +++ b/rsconcept/frontend/src/components/ui/IconValue.tsx @@ -1,5 +1,7 @@ import clsx from 'clsx'; +import { globals } from '@/utils/constants'; + import { CProps } from '../props'; import MiniButton from './MiniButton'; @@ -29,17 +31,12 @@ function IconValue({
- + {value} diff --git a/rsconcept/frontend/src/pages/LibraryPage/LibraryPage.tsx b/rsconcept/frontend/src/pages/LibraryPage/LibraryPage.tsx index 83b75741..05851dde 100644 --- a/rsconcept/frontend/src/pages/LibraryPage/LibraryPage.tsx +++ b/rsconcept/frontend/src/pages/LibraryPage/LibraryPage.tsx @@ -1,9 +1,13 @@ 'use client'; import { AnimatePresence } from 'framer-motion'; +import fileDownload from 'js-file-download'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; +import { IconCSV } from '@/components/Icons'; +import MiniButton from '@/components/ui/MiniButton'; +import Overlay from '@/components/ui/Overlay'; import DataLoader from '@/components/wrap/DataLoader'; import { useAuth } from '@/context/AuthContext'; import { useLibrary } from '@/context/LibraryContext'; @@ -13,7 +17,7 @@ import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/librar import { ILibraryFilter } from '@/models/miscellaneous'; import { storage } from '@/utils/constants'; import { information } from '@/utils/labels'; -import { toggleTristateFlag } from '@/utils/utils'; +import { convertToCSV, toggleTristateFlag } from '@/utils/utils'; import TableLibraryItems from './TableLibraryItems'; import ToolbarSearch from './ToolbarSearch'; @@ -101,6 +105,19 @@ function LibraryPage() { [location, library] ); + const handleDownloadCSV = useCallback(() => { + if (items.length === 0) { + toast.error(information.noDataToExport); + return; + } + const blob = convertToCSV(items); + try { + fileDownload(blob, 'library.csv', 'text/csv;charset=utf-8;'); + } catch (error) { + console.error(error); + } + }, [items]); + const viewLibrary = useMemo( () => ( setShowRenameLocation(false)} /> ) : null} + + } + onClick={handleDownloadCSV} + /> + } value={item.editors.length} - title='Редакторы' onClick={controller.promptEditors} disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER} /> diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx index 83c18b5b..acc5211a 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx @@ -1,13 +1,20 @@ 'use client'; import clsx from 'clsx'; -import { useLayoutEffect, useMemo, useState } from 'react'; +import fileDownload from 'js-file-download'; +import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import { toast } from 'react-toastify'; +import { IconCSV } from '@/components/Icons'; import SelectedCounter from '@/components/info/SelectedCounter'; import { type RowSelectionState } from '@/components/ui/DataTable'; +import MiniButton from '@/components/ui/MiniButton'; +import Overlay from '@/components/ui/Overlay'; import AnimateFade from '@/components/wrap/AnimateFade'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { ConstituentaID, CstType } from '@/models/rsform'; +import { information } from '@/utils/labels'; +import { convertToCSV } from '@/utils/utils'; import { useRSEdit } from '../RSEditContext'; import TableRSList from './TableRSList'; @@ -34,6 +41,19 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) { } }, [controller.selected, controller.schema]); + const handleDownloadCSV = useCallback(() => { + if (!controller.schema || controller.schema.items.length === 0) { + toast.error(information.noDataToExport); + return; + } + const blob = convertToCSV(controller.schema.items); + try { + fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;'); + } catch (error) { + console.error(error); + } + }, [controller]); + function handleRowSelection(updater: React.SetStateAction) { if (!controller.schema) { controller.deselectAll(); @@ -121,6 +141,14 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) { })} /> + + } + onClick={handleDownloadCSV} + /> + + `Копия создана: ${alias}`, + noDataToExport: 'Нет данных для экспорта', addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, newLibraryItem: 'Схема успешно создана', diff --git a/rsconcept/frontend/src/utils/utils.ts b/rsconcept/frontend/src/utils/utils.ts index 123f9b05..e8d45fdd 100644 --- a/rsconcept/frontend/src/utils/utils.ts +++ b/rsconcept/frontend/src/utils/utils.ts @@ -169,3 +169,34 @@ export function extractErrorMessage(error: Error | AxiosError): string { } return error.message; } + +/** + * Convert array of objects to CSV Blob. + */ +export function convertToCSV(targetObj: object[]): Blob { + if (!targetObj || targetObj.length === 0) { + return new Blob([], { type: 'text/csv;charset=utf-8;' }); + } + const separator = ','; + const keys = Object.keys(targetObj[0]); + + const csvContent = + keys.join(separator) + + '\n' + + (targetObj as Record[]) + .map(item => { + return keys + .map(k => { + let cell = item[k] === null || item[k] === undefined ? '' : item[k]; + cell = cell instanceof Date ? cell.toLocaleString() : cell.toString().replace(/"/g, '""'); + if (cell.search(/("|,|\n)/g) >= 0) { + cell = `"${cell}"`; + } + return cell; + }) + .join(separator); + }) + .join('\n'); + + return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); +}