diff --git a/package.json b/package.json index e858479f..84f4c084 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qoretechnologies/reqore", - "version": "0.36.3", + "version": "0.36.4", "description": "ReQore is a highly theme-able and modular UI library for React", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/components/ExportModal/index.tsx b/src/components/ExportModal/index.tsx index c49ab9f0..d325e8fa 100644 --- a/src/components/ExportModal/index.tsx +++ b/src/components/ExportModal/index.tsx @@ -7,7 +7,7 @@ import ReqoreTabs, { IReqoreTabsProps } from '../Tabs'; import ReqoreTabsContent from '../Tabs/content'; export interface IReqoreExportModalProps extends IReqoreModalProps { - data: { [key: string]: any } | any[]; + data: { [key: string]: unknown } | unknown[]; sendNotificationOnCopy?: boolean; tabsOptions?: Omit; } diff --git a/src/components/KeyValueTable/index.tsx b/src/components/KeyValueTable/index.tsx index 22bcedbc..a809894d 100644 --- a/src/components/KeyValueTable/index.tsx +++ b/src/components/KeyValueTable/index.tsx @@ -4,8 +4,14 @@ import { SIZE_TO_PX } from '../../constants/sizes'; import { TReqoreIntent } from '../../constants/theme'; import { IReqoreIconName } from '../../types/icons'; import { IReqoreButtonProps } from '../Button'; +import { IReqoreExportModalProps } from '../ExportModal'; import { IReqorePanelProps } from '../Panel'; -import ReqoreTable, { IReqoreTableColumn, IReqoreTableProps, IReqoreTableRowData } from '../Table'; +import ReqoreTable, { + IReqoreTableColumn, + IReqoreTableProps, + IReqoreTableRowData, + TReqoreTableColumnContent, +} from '../Table'; import { IReqoreTableValueProps, ReqoreTableValue } from '../Table/value'; export type TReqoreKeyValueTablePrimitiveValue = string | number | boolean | null | undefined; @@ -13,6 +19,9 @@ export type TReqoreKeyValueTableValue = | TReqoreKeyValueTablePrimitiveValue | TReqoreKeyValueTablePrimitiveValue[] | { [key: string]: TReqoreKeyValueTableValue }; +export type TReqoreKeyValueTableExportMapper = ( + data: { tableKey: TReqoreTableColumnContent; value: TReqoreTableColumnContent }[] +) => IReqoreExportModalProps['data']; export interface IReqoreKeyValueTableProps extends IReqorePanelProps, @@ -50,20 +59,25 @@ export interface IReqoreKeyValueTableProps minValueWidth?: number; defaultValueFilter?: string | number; + keyRenderer?: (keyLabel: string | number) => string | number; + valueRenderer?: ( data: IReqoreTableRowData, defaultComponent?: ({ value }: IReqoreTableValueProps) => any ) => any; + + exportMapper?: TReqoreKeyValueTableExportMapper; } export const ReqoreKeyValueTable = ({ data, keyLabel, keyIcon, + keyRenderer, valueLabel, valueIcon, valueRenderer, - keyColumnIntent = 'muted', + keyColumnIntent, minValueWidth = 100, maxKeyWidth = 200, keyAlign = 'left', @@ -92,7 +106,7 @@ export const ReqoreKeyValueTable = ({ }, cell: { - content: 'title', + content: keyRenderer ? ({ tableKey }) => keyRenderer(tableKey) : 'title', }, }, { diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 26e74eec..7dfe506b 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -18,8 +18,9 @@ import { useQueryWithDelay } from '../../hooks/useQueryWithDelay'; import { IReqoreIntent, IReqoreTooltip } from '../../types/global'; import { IReqoreButtonProps, TReqoreBadge } from '../Button'; import { IReqoreDropdownItem } from '../Dropdown/list'; -import { ReqoreExportModal } from '../ExportModal'; +import { IReqoreExportModalProps, ReqoreExportModal } from '../ExportModal'; import ReqoreInput, { IReqoreInputProps } from '../Input'; +import { TReqoreKeyValueTableExportMapper } from '../KeyValueTable'; import { IReqorePanelAction, IReqorePanelProps, IReqorePanelSubAction } from '../Panel'; import ReqoreTableBody from './body'; import ReqoreTableHeader, { IReqoreCustomHeaderCellComponent } from './header'; @@ -43,7 +44,7 @@ import { import { IReqoreTableRowOptions } from './row'; export type TReqoreTableColumnContent = - | React.FC<{ [key: string]: any; _selectId?: string | number; _size: TSizes; _dataId: string }> + | ((data: IReqoreTableRowData) => any) | 'time-ago' | 'tag' | `tag:${TReqoreIntent}` @@ -51,7 +52,9 @@ export type TReqoreTableColumnContent = | 'title' | `title:${TReqoreIntent}` | 'text' - | `text:${TReqoreIntent}`; + | `text:${TReqoreIntent}` + | string + | number; export interface IReqoreTableColumn extends IReqoreIntent { dataId: string; @@ -136,6 +139,10 @@ export interface IReqoreTableProps extends IReqorePanelProps { headerCellComponent?: IReqoreCustomHeaderCellComponent; rowComponent?: IReqoreTableRowOptions['rowComponent']; bodyCellComponent?: IReqoreTableRowOptions['cellComponent']; + + exportMapper?: + | TReqoreKeyValueTableExportMapper + | ((data: unknown[]) => IReqoreExportModalProps['data']); } export interface IReqoreTableStyle { @@ -178,6 +185,26 @@ const StyledTablesWrapper = styled.div` overflow: hidden; `; +export interface IReqoreTableExportModalProps { + data: unknown; + onClose: () => void; + exportMapper?: IReqoreTableProps['exportMapper']; +} + +const ReqoreTableExportModal = ({ data, onClose, exportMapper }) => { + const fixedData = useMemo(() => { + let _fixedData = removeInternalData(data); + + if (exportMapper) { + _fixedData = exportMapper(_fixedData); + } + + return _fixedData; + }, [data, exportMapper]); + + return ; +}; + const ReqoreTable = ({ className, height, @@ -210,6 +237,7 @@ const ReqoreTable = ({ onSelectClick, paging, exportable, + exportMapper, ...rest }: IReqoreTableProps) => { const leftTableRef = useRef(null); @@ -734,11 +762,10 @@ const ReqoreTable = ({ {(pagedData) => ( <> {showExportModal && ( - setShowExportModal(undefined)} + exportMapper={exportMapper} /> )} diff --git a/src/components/Table/row.tsx b/src/components/Table/row.tsx index 143c479f..d168c7c2 100644 --- a/src/components/Table/row.tsx +++ b/src/components/Table/row.tsx @@ -179,7 +179,7 @@ const ReqoreTableRow = ({ ); default: - return {data[dataId]}; + return {content}; } } diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 0469ec2b..eaadc631 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -57,7 +57,12 @@ export const getLineCount = (value: string | null): number => { // to a CSV string export const convertToCSV = (objArray: any[]): string => { const header = Object.keys(objArray[0]).join(','); - const rows = objArray.map((obj) => Object.values(obj).join(',')); + const rows = objArray.map((obj) => + Object.values(obj) + .map((value) => JSON.stringify(value)) + .join(',') + ); + return [header, ...rows].join('\n'); }; diff --git a/src/stories/KeyValueTable/KeyValueTable.stories.tsx b/src/stories/KeyValueTable/KeyValueTable.stories.tsx index 4f32b06f..2ac65373 100644 --- a/src/stories/KeyValueTable/KeyValueTable.stories.tsx +++ b/src/stories/KeyValueTable/KeyValueTable.stories.tsx @@ -1,5 +1,5 @@ import { StoryObj } from '@storybook/react'; -import { userEvent } from '@storybook/testing-library'; +import { fireEvent, userEvent, waitFor, within } from '@storybook/testing-library'; import { noop } from 'lodash'; import ReqoreIcon from '../../components/Icon'; import { IReqoreKeyValueTableProps, ReqoreKeyValueTable } from '../../components/KeyValueTable'; @@ -263,6 +263,12 @@ export const CustomPaging: Story = { }, }; +export const CustomKeyRenderer: Story = { + args: { + keyRenderer: (label) => `${label}_custom`, + }, +}; + export const CustomValueRenderer: Story = { args: { valueRenderer: ({ value, tableKey }, Component): TReqoreTableColumnContent | JSX.Element => { @@ -281,3 +287,25 @@ export const CustomValueRenderer: Story = { }, }, }; + +export const CustomExportMapper: Story = { + args: { + exportable: true, + exportMapper: (data) => { + return data.reduce( + (acc, row) => ({ + ...acc, + [row.tableKey.toString()]: row.value, + }), + {} + ); + }, + }, + play: async ({ canvasElement, ...rest }) => { + const canvas = within(canvasElement); + // @ts-expect-error + await ExportableTable.play({ canvasElement, ...rest }); + await fireEvent.click(canvas.getAllByText('Export full data')[0]); + await waitFor(() => canvas.findAllByText('Export data'), { timeout: 5000 }); + }, +};