From f072edf7014080e010737199eeb6ebb18de7b2ae Mon Sep 17 00:00:00 2001
From: Ivan <8611739+IRBorisov@users.noreply.github.com>
Date: Thu, 22 Aug 2024 22:41:41 +0300
Subject: [PATCH] F: Implement CSV export
---
rsconcept/frontend/src/components/Icons.tsx | 3 +-
.../frontend/src/components/ui/IconValue.tsx | 17 +++++-----
.../src/pages/LibraryPage/LibraryPage.tsx | 26 +++++++++++++++-
.../EditorRSFormCard/EditorLibraryItem.tsx | 1 -
.../RSFormPage/EditorRSList/EditorRSList.tsx | 30 +++++++++++++++++-
rsconcept/frontend/src/utils/labels.ts | 1 +
rsconcept/frontend/src/utils/utils.ts | 31 +++++++++++++++++++
7 files changed, 95 insertions(+), 14 deletions(-)
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;' });
+}