mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
F: Implement CSV export
This commit is contained in:
parent
568761cd61
commit
f072edf701
|
@ -51,6 +51,7 @@ export { BiFirstPage as IconPageFirst } from 'react-icons/bi';
|
||||||
export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
||||||
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
|
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
|
||||||
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
|
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
|
||||||
|
export { PiFileCsv as IconCSV } from 'react-icons/pi';
|
||||||
|
|
||||||
// ==== User status =======
|
// ==== User status =======
|
||||||
export { LuUserCircle2 as IconUser } from 'react-icons/lu';
|
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 { TbHexagons as IconOSS } from 'react-icons/tb';
|
||||||
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
||||||
export { TbTopologyRing as IconConsolidation } 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 { RiParentLine as IconParent } from 'react-icons/ri';
|
||||||
export { BiSpa as IconPredecessor } from 'react-icons/bi';
|
export { BiSpa as IconPredecessor } from 'react-icons/bi';
|
||||||
export { LuArchive as IconArchive } from 'react-icons/lu';
|
export { LuArchive as IconArchive } from 'react-icons/lu';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import MiniButton from './MiniButton';
|
import MiniButton from './MiniButton';
|
||||||
|
|
||||||
|
@ -29,17 +31,12 @@ function IconValue({
|
||||||
<div
|
<div
|
||||||
className={clsx('flex items-center text-right', { 'justify-between gap-6': !dense, 'gap-1': dense }, className)}
|
className={clsx('flex items-center text-right', { 'justify-between gap-6': !dense, 'gap-1': dense }, className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
|
||||||
|
data-tooltip-html={titleHtml}
|
||||||
|
data-tooltip-content={title}
|
||||||
|
data-tooltip-hidden={hideTitle}
|
||||||
>
|
>
|
||||||
<MiniButton
|
<MiniButton noHover noPadding icon={icon} disabled={disabled} onClick={onClick} />
|
||||||
noHover
|
|
||||||
noPadding
|
|
||||||
title={title}
|
|
||||||
titleHtml={titleHtml}
|
|
||||||
hideTitle={hideTitle}
|
|
||||||
icon={icon}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
<span id={id} className='min-w-[1.2rem]'>
|
<span id={id} className='min-w-[1.2rem]'>
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import fileDownload from 'js-file-download';
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
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 DataLoader from '@/components/wrap/DataLoader';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
|
@ -13,7 +17,7 @@ import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/librar
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
import { storage } from '@/utils/constants';
|
import { storage } from '@/utils/constants';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
import { toggleTristateFlag } from '@/utils/utils';
|
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
|
||||||
|
|
||||||
import TableLibraryItems from './TableLibraryItems';
|
import TableLibraryItems from './TableLibraryItems';
|
||||||
import ToolbarSearch from './ToolbarSearch';
|
import ToolbarSearch from './ToolbarSearch';
|
||||||
|
@ -101,6 +105,19 @@ function LibraryPage() {
|
||||||
[location, library]
|
[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(
|
const viewLibrary = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TableLibraryItems
|
<TableLibraryItems
|
||||||
|
@ -142,6 +159,13 @@ function LibraryPage() {
|
||||||
hideWindow={() => setShowRenameLocation(false)}
|
hideWindow={() => setShowRenameLocation(false)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Overlay position='top-[0.25rem] right-0' layer='z-tooltip'>
|
||||||
|
<MiniButton
|
||||||
|
title='Выгрузить в формате CSV'
|
||||||
|
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||||
|
onClick={handleDownloadCSV}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
<ToolbarSearch
|
<ToolbarSearch
|
||||||
total={library.items.length ?? 0}
|
total={library.items.length ?? 0}
|
||||||
filtered={items.length}
|
filtered={items.length}
|
||||||
|
|
|
@ -83,7 +83,6 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
dense
|
dense
|
||||||
icon={<IconEditor size='1.25rem' className='icon-primary' />}
|
icon={<IconEditor size='1.25rem' className='icon-primary' />}
|
||||||
value={item.editors.length}
|
value={item.editors.length}
|
||||||
title='Редакторы'
|
|
||||||
onClick={controller.promptEditors}
|
onClick={controller.promptEditors}
|
||||||
disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER}
|
disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
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 SelectedCounter from '@/components/info/SelectedCounter';
|
||||||
import { type RowSelectionState } from '@/components/ui/DataTable';
|
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 AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import { ConstituentaID, CstType } from '@/models/rsform';
|
import { ConstituentaID, CstType } from '@/models/rsform';
|
||||||
|
import { information } from '@/utils/labels';
|
||||||
|
import { convertToCSV } from '@/utils/utils';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import TableRSList from './TableRSList';
|
import TableRSList from './TableRSList';
|
||||||
|
@ -34,6 +41,19 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
}
|
}
|
||||||
}, [controller.selected, controller.schema]);
|
}, [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<RowSelectionState>) {
|
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
|
||||||
if (!controller.schema) {
|
if (!controller.schema) {
|
||||||
controller.deselectAll();
|
controller.deselectAll();
|
||||||
|
@ -121,6 +141,14 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Overlay position='top-[0.25rem] right-[1rem]' layer='z-tooltip'>
|
||||||
|
<MiniButton
|
||||||
|
title='Выгрузить в формате CSV'
|
||||||
|
icon={<IconCSV size='1.25rem' className='icon-green' />}
|
||||||
|
onClick={handleDownloadCSV}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
<TableRSList
|
<TableRSList
|
||||||
items={controller.schema?.items}
|
items={controller.schema?.items}
|
||||||
maxHeight={tableHeight}
|
maxHeight={tableHeight}
|
||||||
|
|
|
@ -933,6 +933,7 @@ export const information = {
|
||||||
versionRestored: 'Загрузка версии завершена',
|
versionRestored: 'Загрузка версии завершена',
|
||||||
locationRenamed: 'Ваши схемы перемещены',
|
locationRenamed: 'Ваши схемы перемещены',
|
||||||
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
||||||
|
noDataToExport: 'Нет данных для экспорта',
|
||||||
|
|
||||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||||
newLibraryItem: 'Схема успешно создана',
|
newLibraryItem: 'Схема успешно создана',
|
||||||
|
|
|
@ -169,3 +169,34 @@ export function extractErrorMessage(error: Error | AxiosError): string {
|
||||||
}
|
}
|
||||||
return error.message;
|
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<string, string | Date | number>[])
|
||||||
|
.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;' });
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user