mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +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 { 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';
|
||||
|
|
|
@ -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({
|
|||
<div
|
||||
className={clsx('flex items-center text-right', { 'justify-between gap-6': !dense, 'gap-1': dense }, className)}
|
||||
{...restProps}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
>
|
||||
<MiniButton
|
||||
noHover
|
||||
noPadding
|
||||
title={title}
|
||||
titleHtml={titleHtml}
|
||||
hideTitle={hideTitle}
|
||||
icon={icon}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<MiniButton noHover noPadding icon={icon} disabled={disabled} onClick={onClick} />
|
||||
<span id={id} className='min-w-[1.2rem]'>
|
||||
{value}
|
||||
</span>
|
||||
|
|
|
@ -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(
|
||||
() => (
|
||||
<TableLibraryItems
|
||||
|
@ -142,6 +159,13 @@ function LibraryPage() {
|
|||
hideWindow={() => setShowRenameLocation(false)}
|
||||
/>
|
||||
) : 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
|
||||
total={library.items.length ?? 0}
|
||||
filtered={items.length}
|
||||
|
|
|
@ -83,7 +83,6 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
|||
dense
|
||||
icon={<IconEditor size='1.25rem' className='icon-primary' />}
|
||||
value={item.editors.length}
|
||||
title='Редакторы'
|
||||
onClick={controller.promptEditors}
|
||||
disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER}
|
||||
/>
|
||||
|
|
|
@ -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<RowSelectionState>) {
|
||||
if (!controller.schema) {
|
||||
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
|
||||
items={controller.schema?.items}
|
||||
maxHeight={tableHeight}
|
||||
|
|
|
@ -933,6 +933,7 @@ export const information = {
|
|||
versionRestored: 'Загрузка версии завершена',
|
||||
locationRenamed: 'Ваши схемы перемещены',
|
||||
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
||||
noDataToExport: 'Нет данных для экспорта',
|
||||
|
||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||
newLibraryItem: 'Схема успешно создана',
|
||||
|
|
|
@ -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<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