From abd1b5d72848e2ff84f5b6e88e3e3cd594a71b87 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 10:56:00 +0100 Subject: [PATCH 01/14] invert parse/format --- src/imex/export/useExportTable.ts | 4 +-- src/imex/import/parse.spec.ts | 32 +++++++++++------------ src/imex/import/useImportTable.utils.ts | 34 ++++++++++++------------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index 61b86be6..55fb6d5c 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -43,8 +43,8 @@ export const useExportTable = ( let value = get(row, column.accessor as string) - if (meta?.parse) { - value = meta.parse(value, Object.values(row)) + if (meta?.format) { + value = meta.format(value, Object.values(row)) } else if (ARRAY_TYPES.has(valueType!) && Array.isArray(value)) { value = value.join(',') } diff --git a/src/imex/import/parse.spec.ts b/src/imex/import/parse.spec.ts index 1835ee23..737dc5b7 100644 --- a/src/imex/import/parse.spec.ts +++ b/src/imex/import/parse.spec.ts @@ -5,35 +5,35 @@ describe('Import parsing', () => { describe('number', () => { it('should parse normal number', () => { const result = parseCell(12, 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(12) }) it('should parse string number', () => { const result = parseCell('10', 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(10) }) it('should parse 0', () => { const result = parseCell(0, 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(0) }) it('should parse 0 in string', () => { const result = parseCell('0', 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(0) }) it('should consider empty string as undefined', () => { const result = parseCell('', 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(undefined) @@ -41,7 +41,7 @@ describe('Import parsing', () => { it('should throw error if invalid number', () => { const parseInvalid = () => parseCell('invalid', 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(parseInvalid).toThrow() @@ -50,14 +50,14 @@ describe('Import parsing', () => { describe('string', () => { it('should return param value', () => { const result = parseCell('value', 'string', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe('value') }) it('should consider empty string as undefined if `ignoreEmpty` is true', () => { const result = parseCell('', 'number', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(undefined) @@ -66,21 +66,21 @@ describe('Import parsing', () => { describe('number[]', () => { it('should parse uniq number', () => { const result = parseCell('1', 'number[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toEqual([1]) }) it('should consider empty string as empty array', () => { const result = parseCell('', 'number[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: false, }) expect(result).toEqual([]) }) it('should parse multiple number', () => { const result = parseCell('1, 2, 3', 'number[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toEqual([1, 2, 3]) @@ -88,7 +88,7 @@ describe('Import parsing', () => { it('should throw error if invalid value', () => { const parseInvalid = () => parseCell('AA', 'number[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(parseInvalid).toThrow() @@ -96,7 +96,7 @@ describe('Import parsing', () => { it('should throw error if invalid number contained', () => { const parseInvalid = () => parseCell('1,A,3', 'number[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(parseInvalid).toThrow() @@ -105,21 +105,21 @@ describe('Import parsing', () => { describe('string[]', () => { it('should parse uniq value', () => { const result = parseCell('a', 'string[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toEqual(['a']) }) it('should consider empty string as empty array', () => { const result = parseCell('', 'string[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: false, }) expect(result).toEqual([]) }) it('should parse multiple values', () => { const result = parseCell('a,b,c', 'string[]', { - format: (value) => value, + parse: (value) => value, ignoreEmpty: true, }) expect(result).toEqual(['a', 'b', 'c']) diff --git a/src/imex/import/useImportTable.utils.ts b/src/imex/import/useImportTable.utils.ts index b048240e..b32c58af 100644 --- a/src/imex/import/useImportTable.utils.ts +++ b/src/imex/import/useImportTable.utils.ts @@ -57,7 +57,7 @@ const isNotEmptyCell = (cell: any) => cell !== '' && cell != null export const parseCell = ( rawCell: any, type: RowValueTypes, - options: { format: (value: any) => any; ignoreEmpty: boolean } + options: { parse: (value: any) => any; ignoreEmpty: boolean } ): string | number | string[] | number[] | undefined => { if (options.ignoreEmpty && !isNotEmptyCell(rawCell)) { return undefined @@ -65,22 +65,22 @@ export const parseCell = ( switch (type) { case 'number': if (typeof rawCell === 'number') { - return Number(options.format(rawCell)) + return Number(options.parse(rawCell)) } - const newCellValue = Number(options.format(rawCell?.replace(',', '.'))) + const newCellValue = Number(options.parse(rawCell?.replace(',', '.'))) if (Number.isNaN(newCellValue)) { throw new Error(ParsingErrors[ParseCellError.NOT_A_NUMBER]) } return newCellValue case 'number[]': - let formattedNumberArrayCell = options.format(rawCell) - if (!Array.isArray(formattedNumberArrayCell)) { - if (typeof formattedNumberArrayCell !== 'string') { + let parsedNumberArrayCell = options.parse(rawCell) + if (!Array.isArray(parsedNumberArrayCell)) { + if (typeof parsedNumberArrayCell !== 'string') { throw new Error(ParsingErrors[ParseCellError.INVALID]) } - formattedNumberArrayCell = formattedNumberArrayCell.split(',') + parsedNumberArrayCell = parsedNumberArrayCell.split(',') } - return formattedNumberArrayCell + return parsedNumberArrayCell .filter(isNotEmptyCell) .map((value: string | number) => { const transformedValue = Number(value) @@ -91,16 +91,16 @@ export const parseCell = ( }) case 'string[]': - let formattedStringArrayCell = options.format(rawCell) - if (!Array.isArray(formattedStringArrayCell)) { - if (typeof formattedStringArrayCell !== 'string') { + let parsedStringArrayCell = options.parse(rawCell) + if (!Array.isArray(parsedStringArrayCell)) { + if (typeof parsedStringArrayCell !== 'string') { throw new Error(ParsingErrors[ParseCellError.INVALID]) } - formattedStringArrayCell = formattedStringArrayCell.split(',') + parsedStringArrayCell = parsedStringArrayCell.split(',') } - return formattedStringArrayCell.filter(isNotEmptyCell) + return parsedStringArrayCell.filter(isNotEmptyCell) default: - return options.format(rawCell) + return options.parse(rawCell) } } @@ -185,8 +185,8 @@ export const parseRawData = async ( let cellError: string | null = null - const format = (value: any) => - orderedColumns[index]?.meta?.imex?.format?.(value, row) ?? value + const parse = (value: any) => + orderedColumns[index]?.meta?.imex?.parse?.(value, row) ?? value let newCellValue: string | number | string[] | number[] | undefined = rawCell @@ -197,7 +197,7 @@ export const parseRawData = async ( newCellValue = parseCell( rawCell, orderedColumns[index]!.meta!.imex!.type as RowValueTypes, - { format, ignoreEmpty } + { parse, ignoreEmpty } ) // If parsed value is null, throw if required and ignore if not. From c0202d599cbe02b73d8414668afd3aba95604525 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 11:09:10 +0100 Subject: [PATCH 02/14] move imex to root column def --- src/imex/excel.utils.ts | 15 +- src/imex/export/useExportTable.ts | 8 +- src/imex/export/useExportTable.utils.ts | 6 +- src/imex/getImexColumns.ts | 2 +- src/imex/imex.interface.ts | 50 +-- src/imex/imex.spec.ts | 8 +- src/imex/import/useImportTable.columns.tsx | 16 +- src/imex/import/useImportTable.tsx | 486 ++++++++++----------- src/imex/import/useImportTable.utils.ts | 19 +- src/types/Table.ts | 21 +- 10 files changed, 298 insertions(+), 333 deletions(-) diff --git a/src/imex/excel.utils.ts b/src/imex/excel.utils.ts index f13be932..30c03872 100644 --- a/src/imex/excel.utils.ts +++ b/src/imex/excel.utils.ts @@ -43,8 +43,8 @@ export const parseExcelFileData = async (file: File): Promise => { break case CellValueType.Hyperlink: // seems to be badly typed - const text = ((cell.value as Excel.CellHyperlinkValue) - .text as unknown) as Excel.CellRichTextValue + const text = (cell.value as Excel.CellHyperlinkValue) + .text as unknown as Excel.CellRichTextValue data[rowIndex][cellIndex] = text?.richText?.map((t) => t.text).join('') ?? text ?? null break @@ -94,9 +94,9 @@ export const applyValidationRulesAndStyle = ( for (const columnIndex in columns) { const column = columns[columnIndex] const columnNumber = Number(columnIndex) + 1 - const dataValidation = column.meta?.imex?.dataValidation - const isIdentifer = !!column.meta?.imex?.identifier - const note = column.meta?.imex?.note + const dataValidation = column?.imex?.dataValidation + const isIdentifer = !!column?.imex?.identifier + const note = column?.imex?.note if (dataValidation || isIdentifer || note) { const worksheetColumn = worksheet.getColumn(columnNumber) @@ -118,9 +118,8 @@ export const applyValidationRulesAndStyle = ( const worksheetName = capitalize( snakeCase(escape(`${column.Header}`)) ) - const validationValuesWorksheet = worksheet.workbook.addWorksheet( - worksheetName - ) + const validationValuesWorksheet = + worksheet.workbook.addWorksheet(worksheetName) dataValidation.formulae.forEach((f, listColumnIndex) => { const list: string[] = f.replace(/"/g, '').split(',') list.forEach((listEl, rowIndex) => { diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index 55fb6d5c..e399c70d 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -38,13 +38,13 @@ export const useExportTable = ( const imexColumns = getImexColumns(columns) const imexData = data.map((row) => imexColumns.map((column) => { - const meta = column.meta?.imex - const valueType = meta?.type + const imexOptions = column?.imex + const valueType = imexOptions?.type let value = get(row, column.accessor as string) - if (meta?.format) { - value = meta.format(value, Object.values(row)) + if (imexOptions?.format) { + value = imexOptions.format(value, Object.values(row)) } else if (ARRAY_TYPES.has(valueType!) && Array.isArray(value)) { value = value.join(',') } diff --git a/src/imex/export/useExportTable.utils.ts b/src/imex/export/useExportTable.utils.ts index 26146a34..fe60d0d6 100644 --- a/src/imex/export/useExportTable.utils.ts +++ b/src/imex/export/useExportTable.utils.ts @@ -63,10 +63,10 @@ export const exportData = async ( const worksheet = workbook.addWorksheet(filename) worksheet.columns = columns.map((column) => ({ - header: column.Header + (column.meta?.imex?.required ? '*' : ''), + header: column.Header + (column?.imex?.required ? '*' : ''), key: column.id ?? column.Header, - width: column.meta?.imex?.width, - hidden: column.meta?.imex?.hidden, + width: column?.imex?.width, + hidden: column?.imex?.hidden, })) as Excel.Column[] worksheet.addRows(data) diff --git a/src/imex/getImexColumns.ts b/src/imex/getImexColumns.ts index 208b5496..66e23d42 100644 --- a/src/imex/getImexColumns.ts +++ b/src/imex/getImexColumns.ts @@ -20,7 +20,7 @@ export const getImexColumns = ( ) const imexColumns = flatColumns.filter( (column) => - !!column.meta?.imex && + !!column?.imex && COLUMN_ENABLED_CONDITION.includes(column.enabled ?? 'always') ) diff --git a/src/imex/imex.interface.ts b/src/imex/imex.interface.ts index d506b250..d200cc9b 100644 --- a/src/imex/imex.interface.ts +++ b/src/imex/imex.interface.ts @@ -1,5 +1,4 @@ import type * as Excel from 'exceljs' -import { DropEvent } from 'react-dropzone' import { Column } from '../types/Table' @@ -12,7 +11,7 @@ export interface ImportedRowMeta { export type ImportedRow = D & { _rowMeta: ImportedRowMeta } -export type UseImportTableOptions = { +export type UseImportTableOptions = { columns: IMEXColumn[] onUpsertRowError?: (error: Error) => void getOriginalData: () => D[] | Promise @@ -40,19 +39,9 @@ export interface _UseImportTableOptions { onFinish?: (rows: D[]) => void | Promise } -export type UseImportTableParams = { +export type UseImportTableParams = { disabled?: boolean accept?: string[] - /** - * @deprecated - */ - onBeforeDropAccepted?: ( - onFiles: ( - files: File[], - options?: Partial> - ) => Promise - ) => (files: File[], event?: DropEvent) => Promise - /** * Defines the number of upsertRow calls parallelized. * @default 1 @@ -63,23 +52,22 @@ export type UseImportTableParams = { export type RowValueTypes = 'string' | 'number' | 'number[]' | 'string[]' -export type IMEXColumn = Column< - D & { [key: string]: any }, - { - imex?: { - identifier?: boolean - required?: boolean - type?: RowValueTypes - format?: (value: any, row: any[]) => any - parse?: (value: any, row: any[]) => any - width?: number - validate?: (value: any, row: any[]) => string | boolean | null - dataValidation?: Excel.DataValidation - hidden?: boolean - note?: string | Excel.Comment - ignoreEmpty?: boolean - } - } -> +export interface IMEXOptions { + identifier?: boolean + required?: boolean + type?: RowValueTypes + format?: (value: any, row: any[]) => any + parse?: (value: any, row: any[]) => any + width?: number + validate?: (value: any, row: any[]) => string | boolean | null + dataValidation?: Excel.DataValidation + hidden?: boolean + note?: string | Excel.Comment + ignoreEmpty?: boolean +} + +export type IMEXColumn = Column & { + imex?: IMEXOptions +} export type IMEXFileExtensionTypes = 'csv' | 'xls' diff --git a/src/imex/imex.spec.ts b/src/imex/imex.spec.ts index ddea26a4..58738f63 100644 --- a/src/imex/imex.spec.ts +++ b/src/imex/imex.spec.ts @@ -60,19 +60,17 @@ describe('Import/Export (imex)', () => { { Header: 'header', accessor: (originalRow) => originalRow.id, - meta: { imex: {} }, + imex: {}, }, ]) ).toThrow() }) it('it needs string header only', () => { expect(() => - getImexColumns([ - { Header: () => null, accessor: 'id', meta: { imex: {} } }, - ]) + getImexColumns([{ Header: () => null, accessor: 'id', imex: {} }]) ).toThrow() }) - it('it ignore columns without meta.imex field', () => { + it('it ignore columns without imex field', () => { expect( getImexColumns([{ Header: () => null, accessor: 'id' }]) ).toHaveLength(0) diff --git a/src/imex/import/useImportTable.columns.tsx b/src/imex/import/useImportTable.columns.tsx index f25fb562..b2c9d2b7 100644 --- a/src/imex/import/useImportTable.columns.tsx +++ b/src/imex/import/useImportTable.columns.tsx @@ -33,7 +33,7 @@ export const getCompareColumnsFromImexColumns = >( ) => { const { footer = true, statusColumn = true } = options ?? {} const compareColumns = columns - .filter((column) => !column.meta?.imex?.hidden) + .filter((column) => !column?.imex?.hidden) .map((column) => ({ ...column, Footer: footer @@ -63,14 +63,14 @@ export const getCompareColumnsFromImexColumns = >( }) as ReactTable.Renderer>>) : undefined, Cell: ((rawProps) => { - const props = (rawProps as unknown) as CellProps + const props = rawProps as unknown as CellProps const rowMeta = rawProps.row.original?._rowMeta - const Cell = (isFunction(column.Cell) - ? column.Cell - : ({ cell }) =>
{cell.value}
) as React.ComponentType< - CellProps - > + const Cell = ( + isFunction(column.Cell) + ? column.Cell + : ({ cell }) =>
{cell.value}
+ ) as React.ComponentType> // Do not add style on grouped cell if (rawProps.row.isGrouped) { @@ -107,7 +107,7 @@ export const getCompareColumnsFromImexColumns = >( if (isNil(cellPrevVal)) { return ( - + diff --git a/src/imex/import/useImportTable.tsx b/src/imex/import/useImportTable.tsx index 1b324f65..250d753a 100644 --- a/src/imex/import/useImportTable.tsx +++ b/src/imex/import/useImportTable.tsx @@ -1,7 +1,7 @@ import { groupBy as lodashGroupBy, omit } from 'lodash' import pLimit from 'p-limit' import * as React from 'react' -import { DropEvent, useDropzone } from 'react-dropzone' +import { useDropzone } from 'react-dropzone' import { useExpanded, useGroupBy } from 'react-table' import { Button, HeaderBar, notify, prompt, Title } from '@habx/ui-core' @@ -20,7 +20,6 @@ import { getImexColumns } from '../getImexColumns' import { IMEXFileExtensionTypes, ImportedRow, - UseImportTableOptions, UseImportTableParams, } from '../imex.interface' import { IMEXColumn } from '../imex.interface' @@ -52,300 +51,281 @@ export const useImportTable = ( const paramsRef = React.useRef(params) paramsRef.current = params - const onFiles = React.useCallback( - async (files: File[], options: Partial> = {}) => { - const mergedOptions = { - ...paramsRef.current, - ...options, - } as UseImportTableParams - validateOptions(mergedOptions) - - /** - * File - */ - const file = files[0] - const fileType: IMEXFileExtensionTypes = file.type.includes('text/') - ? 'csv' - : 'xls' - - /** - * Table params - */ - const initialColumns = mergedOptions.columns as IMEXColumn< - ImportedRow - >[] - const imexColumns = getImexColumns>(initialColumns) - const diffColumns = - getCompareColumnsFromImexColumns>(imexColumns) + const onFiles = React.useCallback(async (files: File[]) => { + const mergedOptions = { + ...paramsRef.current, + } as UseImportTableParams + validateOptions(mergedOptions) + + /** + * File + */ + const file = files[0] + const fileType: IMEXFileExtensionTypes = file.type.includes('text/') + ? 'csv' + : 'xls' + + /** + * Table params + */ + const initialColumns = mergedOptions.columns as IMEXColumn>[] + const imexColumns = getImexColumns>(initialColumns) + const diffColumns = + getCompareColumnsFromImexColumns>(imexColumns) + + const plugins = mergedOptions.groupBy + ? [useGroupBy, useExpanded, useExpandAll] + : [] + + const initialState: Partial>> = { + groupBy: mergedOptions.groupBy ? [mergedOptions.groupBy] : [], + } + + const parseFilePromise: Promise[]> = new Promise( + async (resolve, reject) => { + try { + let rawData + if (mergedOptions.readFile) { + rawData = await mergedOptions.readFile(file) + } else if (file.type.includes('text/')) { + rawData = await parseCsvFileData(file) + } else { + rawData = await parseExcelFileData(file) + } - const plugins = mergedOptions.groupBy - ? [useGroupBy, useExpanded, useExpandAll] - : [] + const originalData = mergedOptions.getOriginalData + ? await mergedOptions.getOriginalData() + : [] - const initialState: Partial>> = { - groupBy: mergedOptions.groupBy ? [mergedOptions.groupBy] : [], + const parsedData = await parseRawData( + { data: rawData, originalData, columns: imexColumns }, + mergedOptions + ) + resolve(parsedData) + } catch (e) { + reject(e) + } } + ) + + const userInputs = await prompt<{ + message: string + ignoredRows: ImportedRow[] + } | null>(({ onResolve }) => ({ + fullscreen: true, + spacing: 'regular', + Component: () => { + // Parsing + const [parsedData, setParsedData] = React.useState[]>() + React.useEffect(() => { + const asyncParse = async () => { + const data = await parseFilePromise + if (data?.length === 0) { + onResolve({ + message: 'Aucune difference avec les données actuelles', + ignoredRows: [], + }) + } + setParsedData(data) + } + asyncParse() + }, []) + + // Table + const tableInstance = useTable>( + { + columns: diffColumns, + data: parsedData, + initialState, + }, + ...plugins + ) + + const [remainingActionsState, remainingActions] = + useRemainingActionsTime() - const parseFilePromise: Promise[]> = new Promise( - async (resolve, reject) => { + /** + * Prevent leaving while uploading + */ + usePreventLeave(remainingActionsState.loading) + + const handleConfirm = async () => { try { - let rawData - if (mergedOptions.readFile) { - rawData = await mergedOptions.readFile(file) - } else if (file.type.includes('text/')) { - rawData = await parseCsvFileData(file) - } else { - rawData = await parseExcelFileData(file) + const concurrency = mergedOptions.concurrency ?? 1 + + const cleanData = + parsedData + ?.filter((row) => !row._rowMeta.isIgnored) // remove ignored rows + .map((row) => omit(row, ['_rowMeta']) as unknown as D) ?? [] // remove local meta + + const dataToUpsert = mergedOptions.groupBy + ? (Object.values( + lodashGroupBy(cleanData, mergedOptions.groupBy) + ) as D[][]) + : cleanData + + if (mergedOptions.upsertRow) { + remainingActions.initLoading() + const limit = pLimit(concurrency) + remainingActions.setActionsCount(dataToUpsert.length) + const upsertRowFunctions = dataToUpsert.map((data: D | D[]) => + limit(async () => { + try { + // @ts-ignore + await mergedOptions.upsertRow?.(data) + } catch (e) { + if (e instanceof Error) { + mergedOptions.onUpsertRowError?.(e) + console.error(e) // eslint-disable-line + } + } + remainingActions.onActionDone() + }) + ) + await Promise.all(upsertRowFunctions) } - const originalData = mergedOptions.getOriginalData - ? await mergedOptions.getOriginalData() - : [] + // @ts-ignore + await mergedOptions.onFinish?.(dataToUpsert) - const parsedData = await parseRawData( - { data: rawData, originalData, columns: imexColumns }, - mergedOptions - ) - resolve(parsedData) + onResolve({ + message: `Import terminé\n${dataToUpsert.length} ligne(s) importée(s)`, + ignoredRows: + parsedData?.filter((row) => row._rowMeta.isIgnored) ?? [], + }) } catch (e) { - reject(e) + if (e instanceof Error) { + console.error(e) // eslint-disable-line + notify(e.toString()) + } + remainingActions.onError() + onResolve(null) } } - ) - const userInputs = await prompt<{ - message: string - ignoredRows: ImportedRow[] - } | null>(({ onResolve }) => ({ + if (!parsedData) { + return ( + + analyse + Analyse en cours... + + ) + } + + return ( + + {mergedOptions.confirmLightBoxTitle && ( + + + {mergedOptions.confirmLightBoxTitle} + + + )} + {remainingActionsState.loading && ( + + )} + + + + + + + + + + ) + }, + })) + + if (userInputs?.message) { + notify({ message: userInputs.message, markdown: true }) + } + + /** + * Skipped rows export + */ + if ( + !mergedOptions.skipIgnoredRowsExport && + userInputs?.ignoredRows.length + ) { + const [errorFileName] = file.name.split('.') + const errorExportFileName = `${errorFileName}_erreurs` + const ignoredRowsColumns = getCompareColumnsFromImexColumns< + ImportedRow + >(imexColumns, { statusColumn: false, footer: false }) + await prompt(({ onResolve }) => ({ fullscreen: true, spacing: 'regular', Component: () => { - // Parsing - const [parsedData, setParsedData] = React.useState[]>() - React.useEffect(() => { - const asyncParse = async () => { - const data = await parseFilePromise - if (data?.length === 0) { - onResolve({ - message: 'Aucune difference avec les données actuelles', - ignoredRows: [], - }) - } - setParsedData(data) - } - asyncParse() - }, []) - - // Table + const [handleExport] = useExportTable({ + columns: initialColumns, + data: userInputs.ignoredRows, + type: fileType, + }) const tableInstance = useTable>( { - columns: diffColumns, - data: parsedData, + data: userInputs.ignoredRows, + columns: ignoredRowsColumns, initialState, }, ...plugins ) - const [remainingActionsState, remainingActions] = - useRemainingActionsTime() - - /** - * Prevent leaving while uploading - */ - usePreventLeave(remainingActionsState.loading) - - const handleConfirm = async () => { - try { - const concurrency = mergedOptions.concurrency ?? 1 - - const cleanData = - parsedData - ?.filter((row) => !row._rowMeta.isIgnored) // remove ignored rows - .map((row) => omit(row, ['_rowMeta']) as unknown as D) ?? [] // remove local meta - - const dataToUpsert = mergedOptions.groupBy - ? (Object.values( - lodashGroupBy(cleanData, mergedOptions.groupBy) - ) as D[][]) - : cleanData - - if (mergedOptions.upsertRow) { - remainingActions.initLoading() - const limit = pLimit(concurrency) - remainingActions.setActionsCount(dataToUpsert.length) - const upsertRowFunctions = dataToUpsert.map((data: D | D[]) => - limit(async () => { - try { - // @ts-ignore - await mergedOptions.upsertRow?.(data) - } catch (e) { - if (e instanceof Error) { - mergedOptions.onUpsertRowError?.(e) - console.error(e) // eslint-disable-line - } - } - remainingActions.onActionDone() - }) - ) - await Promise.all(upsertRowFunctions) - } - - // @ts-ignore - await mergedOptions.onFinish?.(dataToUpsert) - - onResolve({ - message: `Import terminé\n${dataToUpsert.length} ligne(s) importée(s)`, - ignoredRows: - parsedData?.filter((row) => row._rowMeta.isIgnored) ?? [], - }) - } catch (e) { - if (e instanceof Error) { - console.error(e) // eslint-disable-line - notify(e.toString()) - } - remainingActions.onError() - onResolve(null) - } - } - - if (!parsedData) { - return ( - - analyse - Analyse en cours... - - ) + const handleDownloadClick = () => { + handleExport(errorExportFileName) + onResolve(true) } return ( - {mergedOptions.confirmLightBoxTitle && ( - - - {mergedOptions.confirmLightBoxTitle} - - - )} - {remainingActionsState.loading && ( - - )} - + + Les éléments suivant n'ont pas été importés. + +
+ + Téléchargez les {userInputs.ignoredRows.length} éléments ignorés + afin de corriger les données. + +
- - - + ) }, })) - - if (userInputs?.message) { - notify({ message: userInputs.message, markdown: true }) - } - - /** - * Skipped rows export - */ - if ( - !mergedOptions.skipIgnoredRowsExport && - userInputs?.ignoredRows.length - ) { - const [errorFileName] = file.name.split('.') - const errorExportFileName = `${errorFileName}_erreurs` - const ignoredRowsColumns = getCompareColumnsFromImexColumns< - ImportedRow - >(imexColumns, { statusColumn: false, footer: false }) - await prompt(({ onResolve }) => ({ - fullscreen: true, - spacing: 'regular', - Component: () => { - const [handleExport] = useExportTable({ - columns: initialColumns, - data: userInputs.ignoredRows, - type: fileType, - }) - const tableInstance = useTable>( - { - data: userInputs.ignoredRows, - columns: ignoredRowsColumns, - initialState, - }, - ...plugins - ) - - const handleDownloadClick = () => { - handleExport(errorExportFileName) - onResolve(true) - } - - return ( - - - Les éléments suivant n'ont pas été importés. - -
- - Téléchargez les {userInputs.ignoredRows.length} éléments - ignorés afin de corriger les données. - - -
- - - - - - - ) - }, - })) - } - }, - [] - ) + } + }, []) const onDropRejected = React.useCallback( () => notify('Type de fichier non supporté'), [] ) - const onDropAccepted = React.useCallback( - (files: File[], event?: DropEvent) => - // eslint-disable-next-line deprecation/deprecation - params.onBeforeDropAccepted - ? // eslint-disable-next-line deprecation/deprecation - params.onBeforeDropAccepted(onFiles)(files, event) - : onFiles(files), - [ - onFiles, // eslint-disable-line react-hooks/exhaustive-deps - params.onBeforeDropAccepted, // eslint-disable-line deprecation/deprecation - ] - ) - const dropzone = useDropzone({ accept: DEFAULT_ACCEPT, - onDropAccepted, + onDropAccepted: onFiles, onDropRejected, }) diff --git a/src/imex/import/useImportTable.utils.ts b/src/imex/import/useImportTable.utils.ts index b32c58af..391aeae5 100644 --- a/src/imex/import/useImportTable.utils.ts +++ b/src/imex/import/useImportTable.utils.ts @@ -127,13 +127,13 @@ export const parseRawData = async ( throw new Error('Missing headers row') } const identifierColumn = params.columns.find( - (column) => column.meta?.imex?.identifier + (column) => column?.imex?.identifier ) if (!identifierColumn) { throw new Error('Missing identifier column') } const requiredColumnHeaders = params.columns - .filter((column) => column.meta?.imex?.required) + .filter((column) => column?.imex?.required) .map((column) => cleanHeader(column.Header as string)) const missingRequiredColumns = difference( @@ -186,31 +186,30 @@ export const parseRawData = async ( let cellError: string | null = null const parse = (value: any) => - orderedColumns[index]?.meta?.imex?.parse?.(value, row) ?? value + orderedColumns[index]?.imex?.parse?.(value, row) ?? value let newCellValue: string | number | string[] | number[] | undefined = rawCell - const ignoreEmpty = orderedColumns[index]?.meta?.imex?.ignoreEmpty ?? true + const ignoreEmpty = orderedColumns[index]?.imex?.ignoreEmpty ?? true try { newCellValue = parseCell( rawCell, - orderedColumns[index]!.meta!.imex!.type as RowValueTypes, + orderedColumns[index]!.imex!.type as RowValueTypes, { parse, ignoreEmpty } ) // If parsed value is null, throw if required and ignore if not. if (newCellValue == null) { - if (orderedColumns[index]?.meta?.imex?.required) { + if (orderedColumns[index]?.imex?.required) { throw new Error(ParsingErrors[ParseCellError.REQUIRED]) } else if (ignoreEmpty) { continue } } - const validate = - orderedColumns[index]?.meta?.imex?.validate ?? (() => true) + const validate = orderedColumns[index]?.imex?.validate ?? (() => true) const validateResponse = validate(newCellValue, row) const isValid = typeof validateResponse === 'string' @@ -325,7 +324,9 @@ export const parseRawData = async ( }) } -export const validateOptions = (options: UseImportTableParams) => { +export const validateOptions = ( + options: UseImportTableParams +) => { if (options.concurrency && options.concurrency < 1) { throw new Error('concurrency should be greater than 1') } diff --git a/src/types/Table.ts b/src/types/Table.ts index c31efa71..3587ce3d 100644 --- a/src/types/Table.ts +++ b/src/types/Table.ts @@ -83,7 +83,7 @@ type ColumnCustom = Omit< headerClassName?: string } -export type Column = +export type Column = | (Omit, 'columns'> & ColumnCustom) | (ColumnWithLooseAccessor & ColumnCustom) | (ColumnWithStrictAccessor & ColumnCustom) @@ -151,16 +151,15 @@ export interface TableInstance columns: ColumnInstance[] } -export type ColumnInstance< - D extends object = {} -> = ReactTable.ColumnInstance & - ReactTable.UseTableColumnProps & - ReactTable.UseFiltersColumnProps & - ReactTable.UseSortByColumnProps & - ReactTable.UseGroupByColumnProps & - UseDensityColumnProps & { - getToggleAllRowsSelectedProps: Function - } & CustomColumnFields +export type ColumnInstance = + ReactTable.ColumnInstance & + ReactTable.UseTableColumnProps & + ReactTable.UseFiltersColumnProps & + ReactTable.UseSortByColumnProps & + ReactTable.UseGroupByColumnProps & + UseDensityColumnProps & { + getToggleAllRowsSelectedProps: Function + } & CustomColumnFields export interface Cell extends ReactTable.Cell { canGroupBy?: boolean From 251bbde6b57292bf74fadd01d660c11b75477161 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 11:16:04 +0100 Subject: [PATCH 03/14] improve useImport table --- src/imex/import/useImportTable.tsx | 47 +++++++++++++----------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/src/imex/import/useImportTable.tsx b/src/imex/import/useImportTable.tsx index 250d753a..4f009789 100644 --- a/src/imex/import/useImportTable.tsx +++ b/src/imex/import/useImportTable.tsx @@ -52,10 +52,8 @@ export const useImportTable = ( paramsRef.current = params const onFiles = React.useCallback(async (files: File[]) => { - const mergedOptions = { - ...paramsRef.current, - } as UseImportTableParams - validateOptions(mergedOptions) + const options = paramsRef.current + validateOptions(options) /** * File @@ -68,38 +66,38 @@ export const useImportTable = ( /** * Table params */ - const initialColumns = mergedOptions.columns as IMEXColumn>[] + const initialColumns = options.columns as IMEXColumn>[] const imexColumns = getImexColumns>(initialColumns) const diffColumns = getCompareColumnsFromImexColumns>(imexColumns) - const plugins = mergedOptions.groupBy + const plugins = options.groupBy ? [useGroupBy, useExpanded, useExpandAll] : [] const initialState: Partial>> = { - groupBy: mergedOptions.groupBy ? [mergedOptions.groupBy] : [], + groupBy: options.groupBy ? [options.groupBy] : [], } const parseFilePromise: Promise[]> = new Promise( async (resolve, reject) => { try { let rawData - if (mergedOptions.readFile) { - rawData = await mergedOptions.readFile(file) + if (options.readFile) { + rawData = await options.readFile(file) } else if (file.type.includes('text/')) { rawData = await parseCsvFileData(file) } else { rawData = await parseExcelFileData(file) } - const originalData = mergedOptions.getOriginalData - ? await mergedOptions.getOriginalData() + const originalData = options.getOriginalData + ? await options.getOriginalData() : [] const parsedData = await parseRawData( { data: rawData, originalData, columns: imexColumns }, - mergedOptions + options ) resolve(parsedData) } catch (e) { @@ -151,20 +149,20 @@ export const useImportTable = ( const handleConfirm = async () => { try { - const concurrency = mergedOptions.concurrency ?? 1 + const concurrency = options.concurrency ?? 1 const cleanData = parsedData ?.filter((row) => !row._rowMeta.isIgnored) // remove ignored rows .map((row) => omit(row, ['_rowMeta']) as unknown as D) ?? [] // remove local meta - const dataToUpsert = mergedOptions.groupBy + const dataToUpsert = options.groupBy ? (Object.values( - lodashGroupBy(cleanData, mergedOptions.groupBy) + lodashGroupBy(cleanData, options.groupBy) ) as D[][]) : cleanData - if (mergedOptions.upsertRow) { + if (options.upsertRow) { remainingActions.initLoading() const limit = pLimit(concurrency) remainingActions.setActionsCount(dataToUpsert.length) @@ -172,10 +170,10 @@ export const useImportTable = ( limit(async () => { try { // @ts-ignore - await mergedOptions.upsertRow?.(data) + await options.upsertRow?.(data) } catch (e) { if (e instanceof Error) { - mergedOptions.onUpsertRowError?.(e) + options.onUpsertRowError?.(e) console.error(e) // eslint-disable-line } } @@ -186,7 +184,7 @@ export const useImportTable = ( } // @ts-ignore - await mergedOptions.onFinish?.(dataToUpsert) + await options.onFinish?.(dataToUpsert) onResolve({ message: `Import terminé\n${dataToUpsert.length} ligne(s) importée(s)`, @@ -217,11 +215,9 @@ export const useImportTable = ( return ( - {mergedOptions.confirmLightBoxTitle && ( + {options.confirmLightBoxTitle && ( - - {mergedOptions.confirmLightBoxTitle} - + {options.confirmLightBoxTitle} )} {remainingActionsState.loading && ( @@ -260,10 +256,7 @@ export const useImportTable = ( /** * Skipped rows export */ - if ( - !mergedOptions.skipIgnoredRowsExport && - userInputs?.ignoredRows.length - ) { + if (!options.skipIgnoredRowsExport && userInputs?.ignoredRows.length) { const [errorFileName] = file.name.split('.') const errorExportFileName = `${errorFileName}_erreurs` const ignoredRowsColumns = getCompareColumnsFromImexColumns< From 91341a2da19fa1fde47ad55d5d68b8b2e9472a15 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 11:27:38 +0100 Subject: [PATCH 04/14] improve typing + export --- src/imex/export/useExportTable.ts | 28 ++++++++++------------------ src/imex/import/useImportTable.tsx | 12 ++++++------ src/types/Table.ts | 17 ++++++++--------- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index e399c70d..163e2945 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -10,30 +10,22 @@ import { import { exportData, ExportDataOptions } from './useExportTable.utils' -export interface UseExportTableParams +export interface UseExportTableParams extends Omit { - data?: D[] + data: D[] columns: IMEXColumn[] + /** + * @default 'xls' + */ type?: IMEXFileExtensionTypes } const ARRAY_TYPES = new Set(['string[]', 'number[]']) -export const useExportTable = ( - params: UseExportTableParams -) => { - // Put params in ref to avoid useless changes of `onFiles` function - const paramsRef = React.useRef(params) - paramsRef.current = params - +export const useExportTable = () => { const downloadTableData = React.useCallback( - (title: string, options?: Partial>) => { - const { data, columns, ...exportOptions } = { - data: [], - type: 'xls', - ...paramsRef.current, - ...options, - } as const + (title: string, options: UseExportTableParams) => { + const { data = [], columns, type = 'xls' } = options const imexColumns = getImexColumns(columns) const imexData = data.map((row) => @@ -49,7 +41,7 @@ export const useExportTable = ( value = value.join(',') } - return exportOptions.type === 'xls' && + return type === 'xls' && valueType === 'number' && !isFinite(value) && value != null @@ -58,7 +50,7 @@ export const useExportTable = ( }) ) - return exportData(title, imexColumns, imexData, exportOptions) + return exportData(title, imexColumns, imexData, { ...options, type }) }, [] ) diff --git a/src/imex/import/useImportTable.tsx b/src/imex/import/useImportTable.tsx index 4f009789..819df18d 100644 --- a/src/imex/import/useImportTable.tsx +++ b/src/imex/import/useImportTable.tsx @@ -266,11 +266,7 @@ export const useImportTable = ( fullscreen: true, spacing: 'regular', Component: () => { - const [handleExport] = useExportTable({ - columns: initialColumns, - data: userInputs.ignoredRows, - type: fileType, - }) + const [handleExport] = useExportTable() const tableInstance = useTable>( { data: userInputs.ignoredRows, @@ -281,7 +277,11 @@ export const useImportTable = ( ) const handleDownloadClick = () => { - handleExport(errorExportFileName) + handleExport(errorExportFileName, { + columns: initialColumns, + data: userInputs.ignoredRows, + type: fileType, + }) onResolve(true) } diff --git a/src/types/Table.ts b/src/types/Table.ts index 3587ce3d..05299b0f 100644 --- a/src/types/Table.ts +++ b/src/types/Table.ts @@ -59,13 +59,12 @@ export type FooterProps = TableInstance & { export type ColumnEnabledCondition = 'always' | 'never' | 'imex-only' -type CustomColumnFields = { +type CustomColumnFields = { align?: 'left' | 'right' | 'center' enabled?: ColumnEnabledCondition | null - meta?: Meta & { [key: string]: any } } -type ColumnCustom = Omit< +type ColumnCustom = Omit< ReactTable.UseFiltersColumnOptions, 'Filter' > & @@ -73,20 +72,20 @@ type ColumnCustom = Omit< ReactTable.UseGroupByColumnOptions & ReactTable.UseGlobalFiltersColumnOptions & ReactTable.UseSortByColumnOptions & - CustomColumnFields & { + CustomColumnFields & { HeaderIcon?: React.ReactNode Filter?: ReactTable.Renderer> Cell?: ReactTable.Renderer> Header?: ReactTable.Renderer> Footer?: ReactTable.Renderer> - columns?: Column[] + columns?: Column[] headerClassName?: string } -export type Column = - | (Omit, 'columns'> & ColumnCustom) - | (ColumnWithLooseAccessor & ColumnCustom) - | (ColumnWithStrictAccessor & ColumnCustom) +export type Column = + | (Omit, 'columns'> & ColumnCustom) + | (ColumnWithLooseAccessor & ColumnCustom) + | (ColumnWithStrictAccessor & ColumnCustom) export interface TableOptions extends Omit< From 5690200bc51b5caf4f3f69171459cb3d318f02e0 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 11:48:10 +0100 Subject: [PATCH 05/14] simplify imex properties --- src/_fakeData/storyFakeData.tsx | 38 ++++++++++++------------- src/imex/export/useExportTable.ts | 12 ++++---- src/imex/getImexColumns.ts | 8 +++--- src/imex/imex.interface.ts | 7 ++++- src/imex/imex.spec.ts | 34 +++++++++------------- src/imex/import/useImportTable.tsx | 2 +- src/imex/import/useImportTable.utils.ts | 8 ++---- 7 files changed, 51 insertions(+), 58 deletions(-) diff --git a/src/_fakeData/storyFakeData.tsx b/src/_fakeData/storyFakeData.tsx index 16bac139..e3000532 100644 --- a/src/_fakeData/storyFakeData.tsx +++ b/src/_fakeData/storyFakeData.tsx @@ -70,39 +70,39 @@ export const IMEX_COLUMNS = [ { Header: 'Username', accessor: 'username', - meta: { - imex: { - identifier: true, - type: 'string' as const, - }, + imex: { + path: 'username', + header: 'Username', + identifier: true, + type: 'string' as const, }, }, { Header: 'Name', accessor: 'name', - meta: { - imex: { - type: 'string' as const, - note: 'This is a comment', - }, + imex: { + path: 'name', + header: 'Name', + type: 'string' as const, + note: 'This is a comment', }, }, { Header: 'Email', accessor: 'email', - meta: { - imex: { - type: 'string' as const, - }, + imex: { + path: 'email', + header: 'Email', + type: 'string' as const, }, }, { Header: 'City', - accessor: 'address.city', - meta: { - imex: { - type: 'string' as const, - }, + accessor: (row) => row.address.city, + imex: { + path: 'address.city', + header: 'City', + type: 'string' as const, }, }, ] as IMEXColumn[] diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index 163e2945..399c39ad 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -10,7 +10,7 @@ import { import { exportData, ExportDataOptions } from './useExportTable.utils' -export interface UseExportTableParams +export interface UseExportTableParams extends Omit { data: D[] columns: IMEXColumn[] @@ -22,7 +22,7 @@ export interface UseExportTableParams const ARRAY_TYPES = new Set(['string[]', 'number[]']) -export const useExportTable = () => { +export const useExportTable = () => { const downloadTableData = React.useCallback( (title: string, options: UseExportTableParams) => { const { data = [], columns, type = 'xls' } = options @@ -30,12 +30,12 @@ export const useExportTable = () => { const imexColumns = getImexColumns(columns) const imexData = data.map((row) => imexColumns.map((column) => { - const imexOptions = column?.imex - const valueType = imexOptions?.type + const imexOptions = column.imex! + const valueType = imexOptions.type - let value = get(row, column.accessor as string) + let value = get(row, imexOptions.path) - if (imexOptions?.format) { + if (imexOptions.format) { value = imexOptions.format(value, Object.values(row)) } else if (ARRAY_TYPES.has(valueType!) && Array.isArray(value)) { value = value.join(',') diff --git a/src/imex/getImexColumns.ts b/src/imex/getImexColumns.ts index 66e23d42..0172f1f8 100644 --- a/src/imex/getImexColumns.ts +++ b/src/imex/getImexColumns.ts @@ -25,11 +25,11 @@ export const getImexColumns = ( ) imexColumns.forEach((column) => { - if (typeof column.accessor !== 'string') { - throw new Error('Cannot include data with a non-string accessor') + if (typeof column.imex?.path !== 'string') { + throw new Error('Cannot include data without a column path') } - if (typeof column.Header !== 'string') { - throw new Error('Cannot include non string Header') + if (typeof column.imex?.header !== 'string') { + throw new Error('Cannot include data without column name') } }) diff --git a/src/imex/imex.interface.ts b/src/imex/imex.interface.ts index d200cc9b..e8a556cf 100644 --- a/src/imex/imex.interface.ts +++ b/src/imex/imex.interface.ts @@ -53,6 +53,11 @@ export type UseImportTableParams = { export type RowValueTypes = 'string' | 'number' | 'number[]' | 'string[]' export interface IMEXOptions { + /** path passed to lodash get & set function to access & define data **/ + path: string + /** header name of the column in the sheet file **/ + header: string + /** identify a uniq row **/ identifier?: boolean required?: boolean type?: RowValueTypes @@ -66,7 +71,7 @@ export interface IMEXOptions { ignoreEmpty?: boolean } -export type IMEXColumn = Column & { +export type IMEXColumn = Column & { imex?: IMEXOptions } diff --git a/src/imex/imex.spec.ts b/src/imex/imex.spec.ts index 58738f63..cc088731 100644 --- a/src/imex/imex.spec.ts +++ b/src/imex/imex.spec.ts @@ -14,17 +14,15 @@ describe('Import/Export (imex)', () => { global.navigator.msSaveBlob = jest.fn() }) it('should allow download data in CSV', async () => { - const { result } = renderHook(() => - useExportTable({ + const { result } = renderHook(() => useExportTable()) + + await act(() => { + const [downloadFile] = result.current + return downloadFile('test', { data: FAKE_DATA, columns: IMEX_COLUMNS, type: 'csv', }) - ) - - await act(() => { - const [downloadFile] = result.current - return downloadFile('test') }) // @ts-ignore expect(global.navigator.msSaveBlob).toHaveBeenCalledWith( @@ -33,17 +31,15 @@ describe('Import/Export (imex)', () => { ) }) it('should allow download data in XLS', async () => { - const { result } = renderHook(() => - useExportTable({ + const { result } = renderHook(() => useExportTable()) + + await act(() => { + const [downloadFile] = result.current + return downloadFile('test', { data: FAKE_DATA, columns: IMEX_COLUMNS, type: 'xls', }) - ) - - await act(() => { - const [downloadFile] = result.current - return downloadFile('test') }) // @ts-ignore expect(global.navigator.msSaveBlob).toHaveBeenCalledWith( @@ -54,22 +50,18 @@ describe('Import/Export (imex)', () => { }) describe('getImexColumns', () => { - it('it needs string accessors only', () => { + it('it needs a path & a name', () => { expect(() => getImexColumns([ { Header: 'header', - accessor: (originalRow) => originalRow.id, + accessor: (originalRow: any) => originalRow.id, + // @ts-ignore imex: {}, }, ]) ).toThrow() }) - it('it needs string header only', () => { - expect(() => - getImexColumns([{ Header: () => null, accessor: 'id', imex: {} }]) - ).toThrow() - }) it('it ignore columns without imex field', () => { expect( getImexColumns([{ Header: () => null, accessor: 'id' }]) diff --git a/src/imex/import/useImportTable.tsx b/src/imex/import/useImportTable.tsx index 819df18d..1603a5a7 100644 --- a/src/imex/import/useImportTable.tsx +++ b/src/imex/import/useImportTable.tsx @@ -266,7 +266,7 @@ export const useImportTable = ( fullscreen: true, spacing: 'regular', Component: () => { - const [handleExport] = useExportTable() + const [handleExport] = useExportTable>() const tableInstance = useTable>( { data: userInputs.ignoredRows, diff --git a/src/imex/import/useImportTable.utils.ts b/src/imex/import/useImportTable.utils.ts index 391aeae5..49e44e1e 100644 --- a/src/imex/import/useImportTable.utils.ts +++ b/src/imex/import/useImportTable.utils.ts @@ -235,16 +235,12 @@ export const parseRawData = async ( importedRowMeta.isIgnored = true set( importedRowMeta.errors, - orderedColumns[index]?.accessor as string, + orderedColumns[index]!.imex!.path, cellError ) } - set( - importedRowValue, - orderedColumns[index]?.accessor as string, - newCellValue - ) + set(importedRowValue, orderedColumns[index]!.imex!.path, newCellValue) } /** From fd3f07a47b4fcf984fd56e68b6bb345b52559c4b Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 11:53:23 +0100 Subject: [PATCH 06/14] improve typing --- src/types/Table.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/Table.ts b/src/types/Table.ts index 05299b0f..d8e0da13 100644 --- a/src/types/Table.ts +++ b/src/types/Table.ts @@ -60,7 +60,9 @@ export type FooterProps = TableInstance & { export type ColumnEnabledCondition = 'always' | 'never' | 'imex-only' type CustomColumnFields = { + /** @default 'left' */ align?: 'left' | 'right' | 'center' + /** @default 'always' */ enabled?: ColumnEnabledCondition | null } @@ -108,7 +110,7 @@ export interface TableOptions columns: Array | IMEXColumn> defaultColumn?: Partial> initialState?: Partial> - data?: D[] | null + data: D[] | null | undefined } export interface TableState From 39867255da5475a028b97637115d676b6e174974 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 11:57:02 +0100 Subject: [PATCH 07/14] use imex header --- src/imex/export/useExportTable.utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/imex/export/useExportTable.utils.ts b/src/imex/export/useExportTable.utils.ts index fe60d0d6..dda879a8 100644 --- a/src/imex/export/useExportTable.utils.ts +++ b/src/imex/export/useExportTable.utils.ts @@ -63,10 +63,10 @@ export const exportData = async ( const worksheet = workbook.addWorksheet(filename) worksheet.columns = columns.map((column) => ({ - header: column.Header + (column?.imex?.required ? '*' : ''), - key: column.id ?? column.Header, - width: column?.imex?.width, - hidden: column?.imex?.hidden, + header: column.imex?.header + (column.imex?.required ? '*' : ''), + key: column.id ?? column.imex, + width: column.imex?.width, + hidden: column.imex?.hidden, })) as Excel.Column[] worksheet.addRows(data) From 72e89d0cc5cb79b3ded7f8b9dc0bfa138a691887 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 12:07:15 +0100 Subject: [PATCH 08/14] change type of colum to enum --- src/_fakeData/storyFakeData.tsx | 4 --- src/imex/export/useExportTable.ts | 9 ++++--- src/imex/imex.interface.ts | 10 ++++++-- src/imex/import/parse.spec.ts | 34 +++++++++++++------------ src/imex/import/useImportTable.utils.ts | 12 ++++----- src/index.ts | 2 +- 6 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/_fakeData/storyFakeData.tsx b/src/_fakeData/storyFakeData.tsx index e3000532..00fe0e9a 100644 --- a/src/_fakeData/storyFakeData.tsx +++ b/src/_fakeData/storyFakeData.tsx @@ -74,7 +74,6 @@ export const IMEX_COLUMNS = [ path: 'username', header: 'Username', identifier: true, - type: 'string' as const, }, }, { @@ -83,7 +82,6 @@ export const IMEX_COLUMNS = [ imex: { path: 'name', header: 'Name', - type: 'string' as const, note: 'This is a comment', }, }, @@ -93,7 +91,6 @@ export const IMEX_COLUMNS = [ imex: { path: 'email', header: 'Email', - type: 'string' as const, }, }, { @@ -102,7 +99,6 @@ export const IMEX_COLUMNS = [ imex: { path: 'address.city', header: 'City', - type: 'string' as const, }, }, ] as IMEXColumn[] diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index 399c39ad..10d66f06 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -5,7 +5,7 @@ import { getImexColumns } from '../getImexColumns' import { IMEXColumn, IMEXFileExtensionTypes, - RowValueTypes, + IMEXColumnType, } from '../imex.interface' import { exportData, ExportDataOptions } from './useExportTable.utils' @@ -20,7 +20,10 @@ export interface UseExportTableParams type?: IMEXFileExtensionTypes } -const ARRAY_TYPES = new Set(['string[]', 'number[]']) +const ARRAY_TYPES = new Set([ + IMEXColumnType['string[]'], + IMEXColumnType['number[]'], +]) export const useExportTable = () => { const downloadTableData = React.useCallback( @@ -42,7 +45,7 @@ export const useExportTable = () => { } return type === 'xls' && - valueType === 'number' && + valueType === IMEXColumnType.number && !isFinite(value) && value != null ? Number(value) diff --git a/src/imex/imex.interface.ts b/src/imex/imex.interface.ts index e8a556cf..5ba8f62f 100644 --- a/src/imex/imex.interface.ts +++ b/src/imex/imex.interface.ts @@ -50,7 +50,12 @@ export type UseImportTableParams = { concurrency?: number } & UseImportTableOptions -export type RowValueTypes = 'string' | 'number' | 'number[]' | 'string[]' +export enum IMEXColumnType { + 'string', + 'number', + 'number[]', + 'string[]', +} export interface IMEXOptions { /** path passed to lodash get & set function to access & define data **/ @@ -60,7 +65,8 @@ export interface IMEXOptions { /** identify a uniq row **/ identifier?: boolean required?: boolean - type?: RowValueTypes + /** @default IMEXColumnType.string **/ + type?: IMEXColumnType format?: (value: any, row: any[]) => any parse?: (value: any, row: any[]) => any width?: number diff --git a/src/imex/import/parse.spec.ts b/src/imex/import/parse.spec.ts index 737dc5b7..81e9e24d 100644 --- a/src/imex/import/parse.spec.ts +++ b/src/imex/import/parse.spec.ts @@ -1,38 +1,40 @@ +import { IMEXColumnType } from '../imex.interface' + import { parseCell } from './useImportTable.utils' describe('Import parsing', () => { describe('parse cell', () => { describe('number', () => { it('should parse normal number', () => { - const result = parseCell(12, 'number', { + const result = parseCell(12, IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(12) }) it('should parse string number', () => { - const result = parseCell('10', 'number', { + const result = parseCell('10', IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(10) }) it('should parse 0', () => { - const result = parseCell(0, 'number', { + const result = parseCell(0, IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(0) }) it('should parse 0 in string', () => { - const result = parseCell('0', 'number', { + const result = parseCell('0', IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe(0) }) it('should consider empty string as undefined', () => { - const result = parseCell('', 'number', { + const result = parseCell('', IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) @@ -40,7 +42,7 @@ describe('Import parsing', () => { }) it('should throw error if invalid number', () => { const parseInvalid = () => - parseCell('invalid', 'number', { + parseCell('invalid', IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) @@ -49,14 +51,14 @@ describe('Import parsing', () => { }) describe('string', () => { it('should return param value', () => { - const result = parseCell('value', 'string', { + const result = parseCell('value', IMEXColumnType.string, { parse: (value) => value, ignoreEmpty: true, }) expect(result).toBe('value') }) it('should consider empty string as undefined if `ignoreEmpty` is true', () => { - const result = parseCell('', 'number', { + const result = parseCell('', IMEXColumnType.number, { parse: (value) => value, ignoreEmpty: true, }) @@ -65,21 +67,21 @@ describe('Import parsing', () => { }) describe('number[]', () => { it('should parse uniq number', () => { - const result = parseCell('1', 'number[]', { + const result = parseCell('1', IMEXColumnType['number[]'], { parse: (value) => value, ignoreEmpty: true, }) expect(result).toEqual([1]) }) it('should consider empty string as empty array', () => { - const result = parseCell('', 'number[]', { + const result = parseCell('', IMEXColumnType['number[]'], { parse: (value) => value, ignoreEmpty: false, }) expect(result).toEqual([]) }) it('should parse multiple number', () => { - const result = parseCell('1, 2, 3', 'number[]', { + const result = parseCell('1, 2, 3', IMEXColumnType['number[]'], { parse: (value) => value, ignoreEmpty: true, }) @@ -87,7 +89,7 @@ describe('Import parsing', () => { }) it('should throw error if invalid value', () => { const parseInvalid = () => - parseCell('AA', 'number[]', { + parseCell('AA', IMEXColumnType['number[]'], { parse: (value) => value, ignoreEmpty: true, }) @@ -95,7 +97,7 @@ describe('Import parsing', () => { }) it('should throw error if invalid number contained', () => { const parseInvalid = () => - parseCell('1,A,3', 'number[]', { + parseCell('1,A,3', IMEXColumnType['number[]'], { parse: (value) => value, ignoreEmpty: true, }) @@ -104,21 +106,21 @@ describe('Import parsing', () => { }) describe('string[]', () => { it('should parse uniq value', () => { - const result = parseCell('a', 'string[]', { + const result = parseCell('a', IMEXColumnType['string[]'], { parse: (value) => value, ignoreEmpty: true, }) expect(result).toEqual(['a']) }) it('should consider empty string as empty array', () => { - const result = parseCell('', 'string[]', { + const result = parseCell('', IMEXColumnType['string[]'], { parse: (value) => value, ignoreEmpty: false, }) expect(result).toEqual([]) }) it('should parse multiple values', () => { - const result = parseCell('a,b,c', 'string[]', { + const result = parseCell('a,b,c', IMEXColumnType['string[]'], { parse: (value) => value, ignoreEmpty: true, }) diff --git a/src/imex/import/useImportTable.utils.ts b/src/imex/import/useImportTable.utils.ts index 49e44e1e..b6e07fbd 100644 --- a/src/imex/import/useImportTable.utils.ts +++ b/src/imex/import/useImportTable.utils.ts @@ -12,7 +12,7 @@ import { IMEXColumn, ImportedRow, ImportedRowMeta, - RowValueTypes, + IMEXColumnType, UseImportTableOptions, UseImportTableParams, } from '../imex.interface' @@ -56,14 +56,14 @@ const isNotEmptyCell = (cell: any) => cell !== '' && cell != null export const parseCell = ( rawCell: any, - type: RowValueTypes, + type: IMEXColumnType, options: { parse: (value: any) => any; ignoreEmpty: boolean } ): string | number | string[] | number[] | undefined => { if (options.ignoreEmpty && !isNotEmptyCell(rawCell)) { return undefined } switch (type) { - case 'number': + case IMEXColumnType.number: if (typeof rawCell === 'number') { return Number(options.parse(rawCell)) } @@ -72,7 +72,7 @@ export const parseCell = ( throw new Error(ParsingErrors[ParseCellError.NOT_A_NUMBER]) } return newCellValue - case 'number[]': + case IMEXColumnType['number[]']: let parsedNumberArrayCell = options.parse(rawCell) if (!Array.isArray(parsedNumberArrayCell)) { if (typeof parsedNumberArrayCell !== 'string') { @@ -90,7 +90,7 @@ export const parseCell = ( return transformedValue }) - case 'string[]': + case IMEXColumnType['string[]']: let parsedStringArrayCell = options.parse(rawCell) if (!Array.isArray(parsedStringArrayCell)) { if (typeof parsedStringArrayCell !== 'string') { @@ -196,7 +196,7 @@ export const parseRawData = async ( try { newCellValue = parseCell( rawCell, - orderedColumns[index]!.imex!.type as RowValueTypes, + orderedColumns[index]!.imex!.type as IMEXColumnType, { parse, ignoreEmpty } ) diff --git a/src/index.ts b/src/index.ts index 8e84ba0c..b1989ac2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,7 @@ export { export { selectFilterFactory } from './filterFactory/selectFilterFactory' // Import / Export -export { RowValueTypes, IMEXColumn } from './imex/imex.interface' +export { IMEXColumnType, IMEXColumn } from './imex/imex.interface' export { useExportTable, UseExportTableParams, From baf56421c0e62984376c59b9639a7901c77b5197 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 12:08:18 +0100 Subject: [PATCH 09/14] remove deprecated methods --- src/filterMethod/arrayFilter.ts | 12 ------------ src/filterMethod/booleanFilter.ts | 18 ------------------ src/index.ts | 6 ------ 3 files changed, 36 deletions(-) delete mode 100644 src/filterMethod/arrayFilter.ts delete mode 100644 src/filterMethod/booleanFilter.ts diff --git a/src/filterMethod/arrayFilter.ts b/src/filterMethod/arrayFilter.ts deleted file mode 100644 index c5dc1f2d..00000000 --- a/src/filterMethod/arrayFilter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FilterType, Row } from 'react-table' - -/** - * @deprecated use filters: 'includes' instead - */ -export const arrayFilter = ( - accessor: (row: Row) => any -): FilterType => (rows, columnId, filterValue) => { - return rows.filter((row) => { - return filterValue?.some((value: any) => `${accessor(row)}`.includes(value)) - }) -} diff --git a/src/filterMethod/booleanFilter.ts b/src/filterMethod/booleanFilter.ts deleted file mode 100644 index 7aa23313..00000000 --- a/src/filterMethod/booleanFilter.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { get, has } from 'lodash' -import { FilterType } from 'react-table' - -/** - * @deprecated use filters: 'equals' instead - */ -export const booleanFilter: FilterType = (rows, id, filterValue) => { - if (filterValue == null) { - return rows - } - - return rows.filter((row) => { - const rawValue = get(row.values, id) - const value = has(rawValue, 'value') ? !!rawValue.value : !!rawValue - - return (value && filterValue === true) || (!value && filterValue === false) - }) -} diff --git a/src/index.ts b/src/index.ts index b1989ac2..6387991c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,6 @@ export { useTable } from './useTable' export { Table, TablePagination } from './Table' -/* - * Filter methods - */ -export { booleanFilter } from './filterMethod/booleanFilter' -export { arrayFilter } from './filterMethod/arrayFilter' - /* * Filter components */ From 2bae4c61ea210121f2fd10635f3f83515bae9afc Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 14:34:43 +0100 Subject: [PATCH 10/14] add fallback types --- src/_fakeData/storyFakeData.tsx | 6 ++-- src/imex/export/useExportTable.ts | 13 ++++--- src/imex/getImexColumns.ts | 18 +++++++--- src/imex/imex.interface.ts | 40 ++++++++++++++++++---- src/imex/imex.spec.ts | 2 +- src/imex/import/useImportTable.columns.tsx | 8 +++-- src/imex/import/useImportTable.utils.ts | 31 ++++++++++------- 7 files changed, 83 insertions(+), 35 deletions(-) diff --git a/src/_fakeData/storyFakeData.tsx b/src/_fakeData/storyFakeData.tsx index 00fe0e9a..32265975 100644 --- a/src/_fakeData/storyFakeData.tsx +++ b/src/_fakeData/storyFakeData.tsx @@ -66,13 +66,12 @@ export const RICH_COLUMNS = [ }, ] as Column[] -export const IMEX_COLUMNS = [ +export const IMEX_COLUMNS: IMEXColumn[] = [ { Header: 'Username', accessor: 'username', imex: { path: 'username', - header: 'Username', identifier: true, }, }, @@ -98,7 +97,6 @@ export const IMEX_COLUMNS = [ accessor: (row) => row.address.city, imex: { path: 'address.city', - header: 'City', }, }, -] as IMEXColumn[] +] diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index 10d66f06..d77d90ec 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -33,12 +33,17 @@ export const useExportTable = () => { const imexColumns = getImexColumns(columns) const imexData = data.map((row) => imexColumns.map((column) => { - const imexOptions = column.imex! - const valueType = imexOptions.type + const imexOptions = column.imex + const valueType = imexOptions?.type - let value = get(row, imexOptions.path) + const path = ( + typeof column.accessor === 'string' + ? column.accessor + : imexOptions!.path + ) as string + let value = get(row, path) - if (imexOptions.format) { + if (imexOptions?.format) { value = imexOptions.format(value, Object.values(row)) } else if (ARRAY_TYPES.has(valueType!) && Array.isArray(value)) { value = value.join(',') diff --git a/src/imex/getImexColumns.ts b/src/imex/getImexColumns.ts index 0172f1f8..eb642756 100644 --- a/src/imex/getImexColumns.ts +++ b/src/imex/getImexColumns.ts @@ -25,11 +25,21 @@ export const getImexColumns = ( ) imexColumns.forEach((column) => { - if (typeof column.imex?.path !== 'string') { - throw new Error('Cannot include data without a column path') + if ( + typeof column.accessor !== 'string' && + typeof column.imex?.path !== 'string' + ) { + throw new Error( + 'Cannot include data without a column path or string accessor' + ) } - if (typeof column.imex?.header !== 'string') { - throw new Error('Cannot include data without column name') + if ( + typeof column.Header !== 'string' && + typeof column.imex?.header !== 'string' + ) { + throw new Error( + 'Cannot include data without column header or imex.header' + ) } }) diff --git a/src/imex/imex.interface.ts b/src/imex/imex.interface.ts index 5ba8f62f..f2d6b9c4 100644 --- a/src/imex/imex.interface.ts +++ b/src/imex/imex.interface.ts @@ -58,10 +58,18 @@ export enum IMEXColumnType { } export interface IMEXOptions { - /** path passed to lodash get & set function to access & define data **/ - path: string - /** header name of the column in the sheet file **/ - header: string + /** + * path passed to lodash get & set function to access & define data + * fallback to column accessor if provided as string + * + */ + path?: string + /** + * header name of the column in the sheet file + * fallback to column Header if provided as string + * + */ + header?: string /** identify a uniq row **/ identifier?: boolean required?: boolean @@ -77,8 +85,28 @@ export interface IMEXOptions { ignoreEmpty?: boolean } -export type IMEXColumn = Column & { +export type IMEXColumn = Omit< + Column, + 'Header' | 'accessor' +> & { imex?: IMEXOptions -} +} & ( + | { + Header: string + } + | { + Header?: Exclude['Header'], 'string'> + imex?: { header: NonNullable } + } + ) & + ( + | { + accessor: string + } + | { + accessor?: Exclude['accessor'], 'string'> + imex?: { path: NonNullable } + } + ) export type IMEXFileExtensionTypes = 'csv' | 'xls' diff --git a/src/imex/imex.spec.ts b/src/imex/imex.spec.ts index cc088731..48e26c76 100644 --- a/src/imex/imex.spec.ts +++ b/src/imex/imex.spec.ts @@ -53,10 +53,10 @@ describe('Import/Export (imex)', () => { it('it needs a path & a name', () => { expect(() => getImexColumns([ + // @ts-ignore { Header: 'header', accessor: (originalRow: any) => originalRow.id, - // @ts-ignore imex: {}, }, ]) diff --git a/src/imex/import/useImportTable.columns.tsx b/src/imex/import/useImportTable.columns.tsx index b2c9d2b7..57fa74ad 100644 --- a/src/imex/import/useImportTable.columns.tsx +++ b/src/imex/import/useImportTable.columns.tsx @@ -32,8 +32,10 @@ export const getCompareColumnsFromImexColumns = >( options?: GetCompareColumnsFromImexColumnsOptions ) => { const { footer = true, statusColumn = true } = options ?? {} - const compareColumns = columns - .filter((column) => !column?.imex?.hidden) + + // @ts-ignore + const compareColumns: Column[] = columns + .filter((column) => !column.imex?.hidden) .map((column) => ({ ...column, Footer: footer @@ -164,5 +166,5 @@ export const getCompareColumnsFromImexColumns = >( }) } - return compareColumns as Column[] + return compareColumns } diff --git a/src/imex/import/useImportTable.utils.ts b/src/imex/import/useImportTable.utils.ts index b6e07fbd..ea9c53cb 100644 --- a/src/imex/import/useImportTable.utils.ts +++ b/src/imex/import/useImportTable.utils.ts @@ -112,6 +112,7 @@ interface ParseDataParams { originalData: D[] columns: IMEXColumn>[] } + export const parseRawData = async ( params: ParseDataParams, options: Pick< @@ -126,8 +127,9 @@ export const parseRawData = async ( if (!headers) { throw new Error('Missing headers row') } + const identifierColumn = params.columns.find( - (column) => column?.imex?.identifier + (column) => column.imex?.identifier ) if (!identifierColumn) { throw new Error('Missing identifier column') @@ -179,37 +181,44 @@ export const parseRawData = async ( const rawRowValues = Object.values(row) for (let index = 0; index < rawRowValues.length; index++) { const rawCell = rawRowValues[index] - if (!orderedColumns[index]) { + const currentColumn = orderedColumns[index] + if (!currentColumn) { continue } + const columnDataPath = ( + typeof currentColumn.Header === 'string' + ? currentColumn.Header + : currentColumn.imex?.path + ) as string + let cellError: string | null = null const parse = (value: any) => - orderedColumns[index]?.imex?.parse?.(value, row) ?? value + currentColumn.imex?.parse?.(value, row) ?? value let newCellValue: string | number | string[] | number[] | undefined = rawCell - const ignoreEmpty = orderedColumns[index]?.imex?.ignoreEmpty ?? true + const ignoreEmpty = currentColumn.imex?.ignoreEmpty ?? true try { newCellValue = parseCell( rawCell, - orderedColumns[index]!.imex!.type as IMEXColumnType, + currentColumn.imex?.type as IMEXColumnType, { parse, ignoreEmpty } ) // If parsed value is null, throw if required and ignore if not. if (newCellValue == null) { - if (orderedColumns[index]?.imex?.required) { + if (currentColumn.imex?.required) { throw new Error(ParsingErrors[ParseCellError.REQUIRED]) } else if (ignoreEmpty) { continue } } - const validate = orderedColumns[index]?.imex?.validate ?? (() => true) + const validate = currentColumn.imex?.validate ?? (() => true) const validateResponse = validate(newCellValue, row) const isValid = typeof validateResponse === 'string' @@ -233,14 +242,10 @@ export const parseRawData = async ( */ if (cellError) { importedRowMeta.isIgnored = true - set( - importedRowMeta.errors, - orderedColumns[index]!.imex!.path, - cellError - ) + set(importedRowMeta.errors, columnDataPath, cellError) } - set(importedRowValue, orderedColumns[index]!.imex!.path, newCellValue) + set(importedRowValue, columnDataPath, newCellValue) } /** From a0d2d410d8a451e6f1aa2a95dde31e080899c02d Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 14:49:36 +0100 Subject: [PATCH 11/14] cleanup --- src/imex/excel.utils.ts | 3 ++- src/imex/export/useExportTable.ts | 8 +++----- src/imex/export/useExportTable.utils.ts | 3 ++- src/imex/imex.utils.ts | 10 ++++++++++ src/imex/import/useImportTable.columns.tsx | 1 + src/imex/import/useImportTable.utils.ts | 11 ++++------- 6 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 src/imex/imex.utils.ts diff --git a/src/imex/excel.utils.ts b/src/imex/excel.utils.ts index 30c03872..3835abcb 100644 --- a/src/imex/excel.utils.ts +++ b/src/imex/excel.utils.ts @@ -5,6 +5,7 @@ import { palette } from '@habx/ui-core' import { createWorkbook, getCellValueTypes } from './exceljs' import { IMEXColumn } from './imex.interface' +import { getHeader } from './imex.utils' export const parseExcelFileData = async (file: File): Promise => { const CellValueType = await getCellValueTypes() @@ -116,7 +117,7 @@ export const applyValidationRulesAndStyle = ( dataValidation.formulae.some((f) => f.length > 255) ) { const worksheetName = capitalize( - snakeCase(escape(`${column.Header}`)) + snakeCase(escape(getHeader(column))) ) const validationValuesWorksheet = worksheet.workbook.addWorksheet(worksheetName) diff --git a/src/imex/export/useExportTable.ts b/src/imex/export/useExportTable.ts index d77d90ec..8616ca75 100644 --- a/src/imex/export/useExportTable.ts +++ b/src/imex/export/useExportTable.ts @@ -7,6 +7,7 @@ import { IMEXFileExtensionTypes, IMEXColumnType, } from '../imex.interface' +import { getPath } from '../imex.utils' import { exportData, ExportDataOptions } from './useExportTable.utils' @@ -36,11 +37,8 @@ export const useExportTable = () => { const imexOptions = column.imex const valueType = imexOptions?.type - const path = ( - typeof column.accessor === 'string' - ? column.accessor - : imexOptions!.path - ) as string + const path = getPath(column) + let value = get(row, path) if (imexOptions?.format) { diff --git a/src/imex/export/useExportTable.utils.ts b/src/imex/export/useExportTable.utils.ts index dda879a8..f8fd47ad 100644 --- a/src/imex/export/useExportTable.utils.ts +++ b/src/imex/export/useExportTable.utils.ts @@ -6,6 +6,7 @@ import { } from '../excel.utils' import { createWorkbook } from '../exceljs' import { IMEXColumn, IMEXFileExtensionTypes } from '../imex.interface' +import { getHeader } from '../imex.utils' const saveFile = ( type: IMEXFileExtensionTypes, @@ -63,7 +64,7 @@ export const exportData = async ( const worksheet = workbook.addWorksheet(filename) worksheet.columns = columns.map((column) => ({ - header: column.imex?.header + (column.imex?.required ? '*' : ''), + header: `${getHeader(column)} ${column.imex?.required ? '*' : ''}`, key: column.id ?? column.imex, width: column.imex?.width, hidden: column.imex?.hidden, diff --git a/src/imex/imex.utils.ts b/src/imex/imex.utils.ts new file mode 100644 index 00000000..e9cce04a --- /dev/null +++ b/src/imex/imex.utils.ts @@ -0,0 +1,10 @@ +import { IMEXColumn } from './imex.interface' + +export const getPath = (column: IMEXColumn) => + (typeof column.accessor === 'string' + ? column.accessor + : column.imex!.path) as string +export const getHeader = (column: IMEXColumn) => + (typeof column.Header === 'string' + ? column.Header + : column.imex!.header) as string diff --git a/src/imex/import/useImportTable.columns.tsx b/src/imex/import/useImportTable.columns.tsx index 57fa74ad..2267917a 100644 --- a/src/imex/import/useImportTable.columns.tsx +++ b/src/imex/import/useImportTable.columns.tsx @@ -27,6 +27,7 @@ interface GetCompareColumnsFromImexColumnsOptions { */ statusColumn?: boolean } + export const getCompareColumnsFromImexColumns = >( columns: IMEXColumn[], options?: GetCompareColumnsFromImexColumnsOptions diff --git a/src/imex/import/useImportTable.utils.ts b/src/imex/import/useImportTable.utils.ts index ea9c53cb..78912715 100644 --- a/src/imex/import/useImportTable.utils.ts +++ b/src/imex/import/useImportTable.utils.ts @@ -16,6 +16,7 @@ import { UseImportTableOptions, UseImportTableParams, } from '../imex.interface' +import { getHeader, getPath } from '../imex.utils' export enum ParseCellError { NOT_A_NUMBER, @@ -136,7 +137,7 @@ export const parseRawData = async ( } const requiredColumnHeaders = params.columns .filter((column) => column?.imex?.required) - .map((column) => cleanHeader(column.Header as string)) + .map((column) => cleanHeader(getHeader(column))) const missingRequiredColumns = difference( requiredColumnHeaders as string[], @@ -149,7 +150,7 @@ export const parseRawData = async ( const ignoredColumns = [] const orderedColumns = headers.map((header) => { const column = params.columns.find((imexColumn) => { - const searchedHeader = cleanHeader(imexColumn.Header as string) + const searchedHeader = cleanHeader(getHeader(imexColumn)) const cleanedHeader = requiredColumnHeaders.includes(searchedHeader) ? header.replace(/\*$/, '') : header @@ -186,11 +187,7 @@ export const parseRawData = async ( continue } - const columnDataPath = ( - typeof currentColumn.Header === 'string' - ? currentColumn.Header - : currentColumn.imex?.path - ) as string + const columnDataPath = getPath(currentColumn) let cellError: string | null = null From e690ec0c80cd963fbd90fbafb04db03b9b560c41 Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 15:31:03 +0100 Subject: [PATCH 12/14] Add sub columns --- src/imex/imex.interface.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/imex/imex.interface.ts b/src/imex/imex.interface.ts index f2d6b9c4..6a3c6be3 100644 --- a/src/imex/imex.interface.ts +++ b/src/imex/imex.interface.ts @@ -89,6 +89,7 @@ export type IMEXColumn = Omit< Column, 'Header' | 'accessor' > & { + columns?: IMEXColumn[] imex?: IMEXOptions } & ( | { From eb5009a2438edd90d9e509063dec8d3478cbbdce Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Thu, 30 Dec 2021 16:54:14 +0100 Subject: [PATCH 13/14] small fixs --- src/imex/import/useImportTable.columns.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/imex/import/useImportTable.columns.tsx b/src/imex/import/useImportTable.columns.tsx index 2267917a..2466f245 100644 --- a/src/imex/import/useImportTable.columns.tsx +++ b/src/imex/import/useImportTable.columns.tsx @@ -6,6 +6,7 @@ import { Tooltip } from '@habx/ui-core' import { CellProps, Column, FooterProps } from '../../types/Table' import { IMEXColumn, ImportedRow } from '../imex.interface' +import { getPath } from '../imex.utils' import { IconIndicator } from './DataIndicators' import { @@ -80,13 +81,13 @@ export const getCompareColumnsFromImexColumns = >( return } - const cellPrevVal = get(rowMeta?.prevVal, column.accessor as string) + const cellPrevVal = get(rowMeta?.prevVal, getPath(column)) const CellContainer: React.FunctionComponent = ({ children }) => { if (!Object.values(rowMeta?.errors ?? {}).length) { return {children} } - const error = get(rowMeta!.errors, column.accessor as string) + const error = get(rowMeta!.errors, getPath(column)) return ( From 1480414d9a16bcabbea640167acd67f46b6ee72f Mon Sep 17 00:00:00 2001 From: Jean Dessane Date: Mon, 3 Jan 2022 09:23:57 +0100 Subject: [PATCH 14/14] small fixs --- src/imex/imex.interface.ts | 20 ++++++++++++++++++-- src/imex/imex.spec.ts | 12 ------------ src/imex/import/useImportTable.columns.tsx | 2 ++ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/imex/imex.interface.ts b/src/imex/imex.interface.ts index 6a3c6be3..3ad51b57 100644 --- a/src/imex/imex.interface.ts +++ b/src/imex/imex.interface.ts @@ -96,7 +96,7 @@ export type IMEXColumn = Omit< Header: string } | { - Header?: Exclude['Header'], 'string'> + Header?: Exclude['Header'], string> imex?: { header: NonNullable } } ) & @@ -105,9 +105,25 @@ export type IMEXColumn = Omit< accessor: string } | { - accessor?: Exclude['accessor'], 'string'> + accessor?: Exclude['accessor'], string> imex?: { path: NonNullable } } ) export type IMEXFileExtensionTypes = 'csv' | 'xls' + +// Type tests + +// @ts-expect-error +const a: IMEXColumn = { // eslint-disable-line + Header: '', + accessor: () => '', + imex: {}, +} + +// @ts-expect-error +const b: IMEXColumn = { // eslint-disable-line + Header: () => '', + accessor: '', + imex: {}, +} diff --git a/src/imex/imex.spec.ts b/src/imex/imex.spec.ts index 48e26c76..e58f12c8 100644 --- a/src/imex/imex.spec.ts +++ b/src/imex/imex.spec.ts @@ -50,18 +50,6 @@ describe('Import/Export (imex)', () => { }) describe('getImexColumns', () => { - it('it needs a path & a name', () => { - expect(() => - getImexColumns([ - // @ts-ignore - { - Header: 'header', - accessor: (originalRow: any) => originalRow.id, - imex: {}, - }, - ]) - ).toThrow() - }) it('it ignore columns without imex field', () => { expect( getImexColumns([{ Header: () => null, accessor: 'id' }]) diff --git a/src/imex/import/useImportTable.columns.tsx b/src/imex/import/useImportTable.columns.tsx index 2466f245..862f9afc 100644 --- a/src/imex/import/useImportTable.columns.tsx +++ b/src/imex/import/useImportTable.columns.tsx @@ -35,6 +35,8 @@ export const getCompareColumnsFromImexColumns = >( ) => { const { footer = true, statusColumn = true } = options ?? {} + // FIXME + // @ts-ignore const compareColumns: Column[] = columns .filter((column) => !column.imex?.hidden)