Skip to content

Commit

Permalink
All the import options work. A little refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikh2000 authored and erikh2000 committed Dec 27, 2024
1 parent e6d27d2 commit 52a0494
Show file tree
Hide file tree
Showing 16 changed files with 185 additions and 138 deletions.
27 changes: 14 additions & 13 deletions src/csv/__tests__/csvExportUtil.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Rowset from "@/sheets/types/Rowset";
import { COMMA, rowArrayToCsvUnicode, rowArrayToCsvUtf8, TAB } from "../csvExportUtil";

describe('csvExportUtil', () => {
Expand Down Expand Up @@ -29,23 +30,23 @@ describe('csvExportUtil', () => {
});

it('throws if fieldNames is empty', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames:string[] = [];
expect(() => rowArrayToCsvUnicode(rowArray, fieldNames, false)).toThrow();
});
});

describe('datasets with no rows', () => {
it('encodes an empty string when addHeader is false and row array is empty', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['one', 'two', 'three'];
const addHeaders = false;
const expected = '';
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes an array of header names when addHeader is true and row array is empty', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['one', 'two', 'three'];
const addHeaders = true;
const expected = 'one\ttwo\tthree\r\n';
Expand Down Expand Up @@ -97,79 +98,79 @@ describe('csvExportUtil', () => {

describe('headings', () => {
it('encodes a heading preserving lower and upper case', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One'];
const addHeaders = true;
const expected = 'One\r\n';
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes a heading preserving interior whitespace', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One Two'];
const addHeaders = true;
const expected = `One Two\r\n`;
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes a heading trimming leading whitespace', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = [' One'];
const addHeaders = true;
const expected = 'One\r\n';
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes a heading trimming trailing whitespace', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One '];
const addHeaders = true;
const expected = 'One\r\n';
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes a heading preserving non-alphanumeric ASCII characters', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One!@#$%^&*()_+'];
const addHeaders = true;
const expected = `One!@#$%^&*()_+\r\n`;
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes a heading preserving quote characters', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One"'];
const addHeaders = true;
const expected = `"One"""\r\n`;
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders)).toBe(expected);
});

it('encodes a heading preserving field delimiter characters when delimiter is comma', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One,'];
const addHeaders = true;
const expected = `"One,"\r\n`;
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders, COMMA)).toBe(expected);
});

it('encodes a heading preserving field delimiter characters when delimiter is tab', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One\tTwo'];
const addHeaders = true;
const expected = `"One\tTwo"\r\n`;
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders, TAB)).toBe(expected);
});

it('encodes a heading preserving row delimeter characters', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One\rTwo\nThree\r\nFour'];
const addHeaders = true;
const expected = `"One\rTwo\nThree\r\nFour"\r\n`;
expect(rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders, TAB)).toBe(expected);
});

it('encodes a heading preserving Unicode (non-ASCII) characters', () => {
const rowArray:any[][] = [];
const rowArray:Rowset = [];
const fieldNames = ['One😀'];
const addHeaders = true;
const expected = 'One😀\r\n';
Expand Down
7 changes: 4 additions & 3 deletions src/csv/csvExportUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { encodeUtf8 } from "@/common/stringUtil";
import Rowset from "@/sheets/types/Rowset";

export const TAB = '\t';
export const COMMA = ',';
Expand Down Expand Up @@ -50,11 +51,11 @@ function _cellValue(value:any, fieldDelimiter:string):string {
return value.toString();
}

function _doAnyRowsHaveDifferentFieldCount(rowArray:any[][], fieldCount:number):boolean {
function _doAnyRowsHaveDifferentFieldCount(rowArray:Rowset, fieldCount:number):boolean {
return rowArray.some(row => row.length !== fieldCount);
}

export function rowArrayToCsvUnicode(rowArray:any[][], fieldNames:string[], addHeaders:boolean, fieldDelimiter:string = DEFAULT_FIELD_DELIMITER):string {
export function rowArrayToCsvUnicode(rowArray:Rowset, fieldNames:string[], addHeaders:boolean, fieldDelimiter:string = DEFAULT_FIELD_DELIMITER):string {
let csv:string = addHeaders ? _concatHeaderRow(fieldNames, fieldDelimiter) : '';
if (fieldNames.length === 0) throw new Error('fieldNames must have at least one element.');
if (_doAnyRowsHaveDifferentFieldCount(rowArray, fieldNames.length)) throw new Error('All rows must have the same number of fields as fieldNames.');
Expand All @@ -67,7 +68,7 @@ export function rowArrayToCsvUnicode(rowArray:any[][], fieldNames:string[], addH
return csv;
}

export function rowArrayToCsvUtf8(rowArray:any[][], fieldNames:string[], addHeaders:boolean, fieldDelimiter:string = DEFAULT_FIELD_DELIMITER):Uint8Array {
export function rowArrayToCsvUtf8(rowArray:Rowset, fieldNames:string[], addHeaders:boolean, fieldDelimiter:string = DEFAULT_FIELD_DELIMITER):Uint8Array {
let csvUnicode:string = rowArrayToCsvUnicode(rowArray, fieldNames, addHeaders, fieldDelimiter);
return encodeUtf8(csvUnicode);
}
14 changes: 8 additions & 6 deletions src/csv/csvImportUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { decodeUtf8, fillTemplate } from "@/common/stringUtil";
import { decodeUtf8 } from "@/common/stringUtil";
import AppException from "@/common/types/AppException";
import Row from "@/sheets/types/Row";
import Rowset from "@/sheets/types/Rowset";

export enum CvsImportErrorType {
NO_DATA = 'CvsImportError-NO_DATA',
Expand Down Expand Up @@ -193,7 +195,7 @@ function _fieldTextToValue(text:string, rowNo:number):any {
return _fieldTextToString(text, rowNo);
}

function _parseCsvRow(row:string, fieldDelimiter:string, rowNo:number):any[] {
function _parseCsvRow(row:string, fieldDelimiter:string, rowNo:number):Row {
// Similar to splitting rows, I'll split fields first by the delimiter because it's fast, and then fix mistakes, if any.
let fields = row.split(fieldDelimiter);
let wereAnyFieldsRemoved = false;
Expand All @@ -219,7 +221,7 @@ function _valueToString(value:any):string {
return (value === null) ? '' : value.toString();
}

function _parseHeaderRow(headerLine:string, fieldDelimiter:string):any[] {
function _parseHeaderRow(headerLine:string, fieldDelimiter:string):Row {
const row = _parseCsvRow(headerLine, fieldDelimiter, 1);
for(let fieldI = 0; fieldI < row.length; ++fieldI) {
if (typeof row[fieldI] !== 'string') row[fieldI] = _valueToString(row[fieldI]); // Be forgiving and try to preserve intent.
Expand Down Expand Up @@ -258,12 +260,12 @@ function _generateHeaderRow(fieldCount:number):string[] {
}

// Can throw CvsImportError.NO_DATA, FIELD_COUNT_MISMATCH, UNSTRUCTURED_DATA, TOO_MANY_FIELDS
export function csvUnicodeToRowArray(csvUnicode:string, includeHeaders:boolean):any[][] {
export function csvUnicodeToRowArray(csvUnicode:string, includeHeaders:boolean):Rowset {
if (csvUnicode.trim() === '') throw new AppException(CvsImportErrorType.NO_DATA, 'No data found in CSV text.');

const lines = _splitCsvLines(csvUnicode);
const fieldDelimiter = _findFieldDelimiter(lines);
const rows:any[][] = [];
const rows:Rowset = [];

let fromRowI = 0;
const fieldCount = _countFieldsInLine(lines[0], fieldDelimiter);
Expand All @@ -285,7 +287,7 @@ export function csvUnicodeToRowArray(csvUnicode:string, includeHeaders:boolean):
}

// Can throw CvsImportError.NO_DATA, FIELD_COUNT_MISMATCH, UNSTRUCTURED_DATA, TOO_MANY_FIELDS
export function csvUtf8ToRowArray(csvBytes:Uint8Array, includesHeaders:boolean):any[][] {
export function csvUtf8ToRowArray(csvBytes:Uint8Array, includesHeaders:boolean):Rowset {
const csvUnicode = decodeUtf8(csvBytes);
return csvUnicodeToRowArray(csvUnicode, includesHeaders);
}
20 changes: 8 additions & 12 deletions src/homeScreen/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useEffect, useState } from "react";
import { WorkBook } from 'xlsx';

import styles from './HomeScreen.module.css';
import { init } from "./interactions/initialization";
import ToastPane from "@/components/toasts/ToastPane";
import SheetPane from "./SheetPane";
import ImportSheetDialog from "./dialogs/ImportSheetDialog";
import { importSheet, onCancelImportSheet, onChangeWorkbook, onSelectSheet } from "./interactions/import";
import { importSheet, onSelectSheet } from "./interactions/import";
import PromptPane from "./PromptPane";
import HoneSheet from "@/sheets/types/HoneSheet";
import ExecuteSetupDialog from "./dialogs/ExecuteSetupDialog";
Expand All @@ -27,9 +26,8 @@ import ExportOptionsDialog from "./dialogs/ExportOptionsDialog";
import ImportOptionsDialog from "./dialogs/ImportOptionsDialog";

function HomeScreen() {
const [workbook, setWorkbook] = useState<WorkBook|null>(null);
const [, setWorkbookName] = useState<string>(''); // TODO - use workbookName later for export.
const [sheet, setSheet] = useState<HoneSheet|null>(null);
const [availableSheets, setAvailableSheets] = useState<HoneSheet[]>([]);
const [selectedRowNo, setSelectedRowNo] = useState<number>(1);
const [job, setJob] = useState<ExecutionJob|null>(null);
const [modalDialog, setModalDialog] = useState<string|null>(null);
Expand All @@ -52,18 +50,16 @@ function HomeScreen() {
<h1>Hone</h1>
</div>
<SheetPane
workbook={workbook} sheet={sheet} className={styles.sheetPane} selectedRowNo={selectedRowNo}
sheet={sheet} className={styles.sheetPane} selectedRowNo={selectedRowNo}
onRowSelect={setSelectedRowNo}
onImportSheet={() => setModalDialog(ImportOptionsDialog.name)}
onChangeWorkbook={(nextWorkbook, nextWorkbookName) => onChangeWorkbook(nextWorkbook, nextWorkbookName,
setWorkbook, setWorkbookName, setSheet, setModalDialog)}
onExportSheet={() => chooseExportType(setModalDialog)}
/>
{promptPaneContent}

<ImportSheetDialog workbook={workbook} isOpen={modalDialog === ImportSheetDialog.name}
onChoose={(sheet) => onSelectSheet(sheet, setSheet, setModalDialog)}
onCancel={() => onCancelImportSheet(setWorkbook, setWorkbookName, setSheet, setModalDialog)}
<ImportSheetDialog availableSheets={availableSheets} isOpen={modalDialog === ImportSheetDialog.name}
onChoose={(sheet) => onSelectSheet(sheet, setAvailableSheets, setSheet, setModalDialog)}
onCancel={() => setModalDialog(null)}
/>

<ResumeJobDialog isOpen={modalDialog === ResumeJobDialog.name} job={job}
Expand Down Expand Up @@ -95,8 +91,8 @@ function HomeScreen() {
/>

<ImportOptionsDialog
isOpen={modalDialog === ImportOptionsDialog.name}
onImport={(importOptions) => importSheet(importOptions, setSheet, setModalDialog)}
isOpen={modalDialog === ImportOptionsDialog.name} sheet={sheet}
onImport={(importOptions) => importSheet(importOptions, setAvailableSheets, setSheet, setModalDialog)}
onCancel={() => setModalDialog(null)}
/>

Expand Down
5 changes: 3 additions & 2 deletions src/homeScreen/PromptOutputRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import HoneColumn from "@/sheets/types/HoneColumn";
import HoneSheet from "@/sheets/types/HoneSheet";
import styles from './PromptOutputRow.module.css';
import GeneratedText from "@/components/generatedText/GeneratedText";
import Row from "@/sheets/types/Row";

type Props = {
sheet:HoneSheet;
Expand All @@ -15,7 +16,7 @@ function _tableHeaderContent(columns:HoneColumn[]) {
return <tr><th key={-1}>#</th>{columns.map((column, i) => <th key={i}>{column.name}</th>)}</tr>;
}

function _tableBodyContent(row:any[], rowNo:number) {
function _tableBodyContent(row:Row, rowNo:number) {
const cells = row.map((cell:any, columnI:number) => {
const cellValue = (columnI === row.length - 1) ? <GeneratedText text={'' + cell}/> : '' + cell;
return (<td key={columnI}>{cellValue}</td>);
Expand All @@ -25,7 +26,7 @@ function _tableBodyContent(row:any[], rowNo:number) {

function PromptOutputRow({sheet, rowNo, outputValue}: Props) {
const [columns, setColumns] = useState<HoneColumn[]>([]);
const [row, setRow] = useState<any[]>([]);
const [row, setRow] = useState<Row>([]);

useEffect(() => {
if (!sheet) { setColumns([]); setRow([]); return; }
Expand Down
13 changes: 1 addition & 12 deletions src/homeScreen/SheetPane.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { WorkBook } from "xlsx";
import { useEffect } from "react";

import SheetView from "./SheetView";
import { errorToast } from "@/components/toasts/toastUtil";
import Pane, { ButtonDefinition } from "@/components/pane/Pane";
import { getComment } from "./interactions/comment";
import HoneSheet from "@/sheets/types/HoneSheet";

type Props = {
sheet: HoneSheet|null,
workbook: WorkBook|null,
className:string,
selectedRowNo:number,
onRowSelect:(rowNo:number)=>void
onChangeWorkbook(workbook:WorkBook, workbookName:string):void,
onExportSheet():void,
onImportSheet():void
}
Expand All @@ -27,12 +21,7 @@ function _sheetContent(sheet:HoneSheet|null, selectedRowNo:number, onRowSelect:(
return <SheetView sheet={sheet} selectedRowNo={selectedRowNo} onRowSelect={onRowSelect}/>;
}

function SheetPane({workbook, sheet, className, onImportSheet, selectedRowNo, onRowSelect, onExportSheet}:Props) {
useEffect(() => {
if (!workbook) return;
if (!workbook.SheetNames.length) errorToast('Workbook has no sheets.');
}, [workbook]);

function SheetPane({sheet, className, onImportSheet, selectedRowNo, onRowSelect, onExportSheet}:Props) {
const content = _sheetContent(sheet, selectedRowNo, onRowSelect);

const buttons:ButtonDefinition[] = [
Expand Down
3 changes: 2 additions & 1 deletion src/homeScreen/SheetView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import HoneSheet from "@/sheets/types/HoneSheet";
import HoneColumn from "@/sheets/types/HoneColumn";
import { getSheetRows } from "@/sheets/sheetUtil";
import GeneratedText from "@/components/generatedText/GeneratedText";
import Rowset from "@/sheets/types/Rowset";

type Props = {
sheet: HoneSheet,
Expand All @@ -19,7 +20,7 @@ function _tableHeaderContent(columns:HoneColumn[]) {
return <tr><th key={-1}>#</th>{columns.map((column, i) => <th key={i}>{column.name}</th>)}</tr>;
}

function _tableBodyContent(rows:any[][], generatingColumnI:number, selectedRowNo?:number, onRowSelect?:(rowNo:number)=>void) {
function _tableBodyContent(rows:Rowset, generatingColumnI:number, selectedRowNo?:number, onRowSelect?:(rowNo:number)=>void) {
return rows.map(
(row:any, rowI:number) => {
const isSelected = (selectedRowNo === rowI+1);
Expand Down
2 changes: 1 addition & 1 deletion src/homeScreen/dialogs/ImportOptionsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function ImportOptionsDialog({isOpen, onImport, onCancel, sheet}:Props) {
const importButtonName = IMPORT_BUTTON_NAMES[importOptions.importType];
const sheetNameInput = importOptions.importType !== ImportType.CLIPBOARD ? null
: <DialogTextInput labelText="Sheet Name:" value={importOptions.sheetName} onChangeText={sheetName => setImportOptions({...importOptions, sheetName})} />;
const useFirstRowColumnNames = importOptions.importType === ImportType.EXAMPLE ? null
const useFirstRowColumnNames = importOptions.importType === ImportType.EXAMPLE || importOptions.importType === ImportType.EXCEL ? null
: <Checkbox label="Use first row as column names" isChecked={importOptions.useFirstRowColumnNames}
onChange={useFirstRowColumnNames => setImportOptions({...importOptions, useFirstRowColumnNames}) } />
const importOptionsSection = sheetNameInput === null && useFirstRowColumnNames === null ? null
Expand Down
Loading

0 comments on commit 52a0494

Please sign in to comment.