Skip to content

Commit

Permalink
Direct paste from clipboard flow works.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikh2000 authored and erikh2000 committed Dec 27, 2024
1 parent 98287d8 commit 81170f6
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 27 deletions.
16 changes: 13 additions & 3 deletions src/homeScreen/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";

import styles from './HomeScreen.module.css';
import { init } from "./interactions/initialization";
import { deinit, init } from "./interactions/initialization";
import ToastPane from "@/components/toasts/ToastPane";
import SheetPane from "./SheetPane";
import ImportSheetDialog from "./dialogs/ImportSheetDialog";
Expand All @@ -24,6 +24,7 @@ import KeepPartialDataDialog from "./dialogs/KeepPartialDataDialog";
import ResumeJobDialog from "./dialogs/ResumeJobDialog";
import ExportOptionsDialog from "./dialogs/ExportOptionsDialog";
import ImportOptionsDialog from "./dialogs/ImportOptionsDialog";
import ConfirmSheetPasteDialog from "./dialogs/ConfirmSheetPasteDialog";

function HomeScreen() {
const [sheet, setSheet] = useState<HoneSheet|null>(null);
Expand All @@ -34,8 +35,9 @@ function HomeScreen() {
const [promptTemplate, setPromptTemplate] = useState<string>('');

useEffect(() => {
init().then(() => { });
});
init(setAvailableSheets, setModalDialog).then(() => { });
return deinit;
}, []);

const promptPaneContent = sheet ?
<PromptPane
Expand Down Expand Up @@ -96,6 +98,14 @@ function HomeScreen() {
onCancel={() => setModalDialog(null)}
/>

<ConfirmSheetPasteDialog
isOpen={modalDialog === ConfirmSheetPasteDialog.name}
pastedSheet={availableSheets[0]}
existingSheet={sheet}
onConfirm={(pastedSheet) => onSelectSheet(pastedSheet, setAvailableSheets, setSheet, setModalDialog)}
onCancel={() => setModalDialog(null)}
/>

<ToastPane/>
</div>
);
Expand Down
36 changes: 18 additions & 18 deletions src/homeScreen/SheetView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState, useEffect } from "react";
import { useMemo } from "react";

import styles from './SheetView.module.css';
import HoneSheet from "@/sheets/types/HoneSheet";
import HoneColumn from "@/sheets/types/HoneColumn";
import { getSheetRows } from "@/sheets/sheetUtil";
import { getSheetRows, HTML_NBSP } from "@/sheets/sheetUtil";
import GeneratedText from "@/components/generatedText/GeneratedText";
import Rowset from "@/sheets/types/Rowset";
import { plural } from "@/common/englishGrammarUtil";

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

function _tableBodyContent(rows:Rowset, generatingColumnI:number, selectedRowNo?:number, onRowSelect?:(rowNo:number)=>void) {
function _paddedRowContent(rowI:number, columnCount:number) {
const cells = Array(columnCount).fill(null).map((_, i) => <td key={i}></td>);
return (<tr key={rowI}><td key={-1}>{HTML_NBSP}</td>{cells}</tr>);
}

function _tableBodyContent(rows:Rowset, sheetRowCount:number, generatingColumnI:number, selectedRowNo?:number, onRowSelect?:(rowNo:number)=>void) {
return rows.map(
(row:any, rowI:number) => {
if (rowI >= sheetRowCount) return _paddedRowContent(rowI, row.length);

const isSelected = (selectedRowNo === rowI+1);
const rowStyle = isSelected ? styles.selectedRow : '';
const cells = row.map((cell:any, columnI:number) => {
Expand All @@ -38,29 +46,21 @@ function _tableBodyContent(rows:Rowset, generatingColumnI:number, selectedRowNo?
}

function SheetView({sheet, maxRows, padToMax, generatingColumnName, selectedRowNo, onRowSelect}:Props) {
const [rows, setRows] = useState<any>(null);
const [generatingColumnI, setGeneratingColumnI] = useState<number>(-1);

useEffect(() => {
let nextRows = getSheetRows(sheet, 0, maxRows, padToMax);
setRows(nextRows);
}, [sheet, maxRows]);

useEffect(() => {
if (!sheet) return;
if (!generatingColumnName) { setGeneratingColumnI(-1); return; }
const nextI = sheet.columns.findIndex((column) => column.name === generatingColumnName);
setGeneratingColumnI(nextI);
const rows = useMemo(() => getSheetRows(sheet, 0, maxRows, padToMax), [sheet, maxRows, padToMax]);
const generatingColumnI = useMemo(() => {
if (!generatingColumnName) return -1;
return sheet.columns.findIndex((column) => column.name === generatingColumnName);
}, [sheet, generatingColumnName]);

if (!rows) return <div>Loading...</div>;
const sheetRowCount = sheet.rows.length;

return (
<div className={styles.sheetTable}>
<table>
<thead>{_tableHeaderContent(sheet.columns)}</thead>
<tbody>{_tableBodyContent(rows, generatingColumnI, selectedRowNo, onRowSelect)}</tbody>
<tfoot><tr><td colSpan={sheet.columns.length+1}><small>{sheet.rows.length} rows</small></td></tr></tfoot>
<tbody>{_tableBodyContent(rows, sheetRowCount, generatingColumnI, selectedRowNo, onRowSelect)}</tbody>
<tfoot><tr><td colSpan={sheet.columns.length+1}><small>{sheetRowCount} {plural('row', sheetRowCount)}</small></td></tr></tfoot>
</table>
</div>
);
Expand Down
42 changes: 42 additions & 0 deletions src/homeScreen/dialogs/ConfirmSheetPasteDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState, useEffect, useMemo } from 'react';

import ModalDialog from '@/components/modalDialogs/ModalDialog';
import DialogFooter from '@/components/modalDialogs/DialogFooter';
import DialogButton from '@/components/modalDialogs/DialogButton';
import SheetView from '../SheetView';
import HoneSheet from '@/sheets/types/HoneSheet';
import { doesSheetHaveWritableColumns } from '@/sheets/sheetUtil';

type Props = {
pastedSheet:HoneSheet|null,
existingSheet:HoneSheet|null,
isOpen:boolean,
onConfirm(sheet:HoneSheet):void,
onCancel():void
}

function _getUiTextAffectedByExistingSheet(existingSheet:HoneSheet|null):{confirmButtonText:string, description:string} {
if (!existingSheet) return {confirmButtonText:'Import', description:'Import this sheet into Hone?'};
let description = `Replace "${existingSheet.name}" sheet with this new pasted sheet?`;
if (doesSheetHaveWritableColumns(existingSheet)) description += ' You will discard any added columns.';
return {confirmButtonText:'Replace', description};
}

function ConfirmSheetPasteDialog({pastedSheet, existingSheet, isOpen, onConfirm, onCancel}:Props) {
if (!isOpen || !pastedSheet) return null;

const {confirmButtonText, description} = _getUiTextAffectedByExistingSheet(existingSheet);

return (
<ModalDialog isOpen={isOpen} onCancel={onCancel} title="Confirm Sheet Paste">
<p>{description}</p>
<SheetView sheet={pastedSheet} maxRows={5} padToMax={true}/>
<DialogFooter>
<DialogButton text="Cancel" onClick={onCancel} />
<DialogButton text={confirmButtonText} onClick={() => { if (pastedSheet) onConfirm(pastedSheet)}} isPrimary/>
</DialogFooter>
</ModalDialog>
);
}

export default ConfirmSheetPasteDialog;
32 changes: 31 additions & 1 deletion src/homeScreen/interactions/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import ImportSheetDialog from '@/homeScreen/dialogs/ImportSheetDialog';
import HoneSheet from '@/sheets/types/HoneSheet';
import ImportOptions from '@/homeScreen/types/ImportOptions';
import ImportType from '@/homeScreen/types/ImportType';
import { importSheetFromClipboard, importSheetFromCsvFile, importSheetsFromXlsBytes, importSheetsFromXlsFile, SheetErrorType } from '@/sheets/sheetUtil';
import { importSheetFromClipboard, importSheetFromClipboardData, importSheetFromCsvFile, importSheetsFromXlsBytes, importSheetsFromXlsFile, SheetErrorType } from '@/sheets/sheetUtil';
import { CvsImportErrorType, MAX_FIELD_COUNT } from '@/csv/csvImportUtil';
import ConfirmSheetPasteDialog from "../dialogs/ConfirmSheetPasteDialog";

async function _selectExcelFileHandle():Promise<FileSystemFileHandle|null> {
const openFileOptions = {
Expand Down Expand Up @@ -84,6 +85,35 @@ async function _importFromClipboard(importOptions:ImportOptions, setSheet:Functi
}
}

export async function importFromPasteEvent(event:ClipboardEvent, setAvailableSheets:Function, setModalDialog:Function) {
const clipboardData = event.clipboardData;
if (!clipboardData) return;

try {
const honeSheet = await importSheetFromClipboardData(clipboardData, 'Pasted');
setAvailableSheets([honeSheet]);
setModalDialog(ConfirmSheetPasteDialog.name);
} catch(e:any) {
setModalDialog(null);
switch(e.name) {
// Most errors will be quietly ignored because the user might have accidentally tried to paste something that wasn't a table.
// They can use the "import" feature to express their intent more clearly and get more feedback on import problems.
case SheetErrorType.CLIPBOARD_NO_ROWS: case CvsImportErrorType.NO_DATA:
case SheetErrorType.UNEXPECTED_CLIPBOARD_ERROR:
case CvsImportErrorType.FIELD_COUNT_MISMATCH: case CvsImportErrorType.UNSTRUCTURED_DATA:
return;

case CvsImportErrorType.TOO_MANY_FIELDS:
errorToast(`The pasted data had too many columns. (Max supported is ${MAX_FIELD_COUNT}). Maybe try copying a smaller set of columns?`);
return;

default:
console.error(e); // Debug error probably.
return;
}
}
}

async function _importFromCsv(importOptions:ImportOptions, setSheet:Function, setModalDialog:Function) {
try {
const fileHandle = await _selectCsvFileHandle();
Expand Down
32 changes: 31 additions & 1 deletion src/homeScreen/interactions/initialization.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import { setSystemMessage } from "@/llm/llmUtil";
import { SYSTEM_MESSAGE } from "./prompt";
import { importFromPasteEvent } from "./import";

export async function init() {
type PasteHandlerFunction = (event:ClipboardEvent) => void;

let pasteHandler:PasteHandlerFunction|null = null;

function _handlePaste(clipboardEvent:ClipboardEvent, setAvailableSheets:Function, setModalDialog:Function) {
const target = clipboardEvent.target as HTMLElement|null;

if (target) {
// If an editable DOM element is focused, I just want the browser's default handling of the paste.
const targetName = target.tagName;
if (targetName === 'INPUT' || targetName === 'TEXTAREA' || target.isContentEditable) return;
}

// Try to import a sheet from the clipboard.
clipboardEvent.preventDefault();
importFromPasteEvent(clipboardEvent, setAvailableSheets, setModalDialog);
}

export async function init(setAvailableSheets:Function, setModalDialog:Function) {
setSystemMessage(SYSTEM_MESSAGE);

if (pasteHandler) document.removeEventListener('paste', pasteHandler);
pasteHandler = clipboardEvent => _handlePaste(clipboardEvent, setAvailableSheets, setModalDialog);
document.addEventListener('paste', pasteHandler);
}

export function deinit() {
if (pasteHandler) {
document.removeEventListener('paste', pasteHandler);
pasteHandler = null;
}
}
37 changes: 33 additions & 4 deletions src/sheets/sheetUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import StringMap from '@/common/types/StringMap';
import HoneSheet from './types/HoneSheet';
import HoneColumn from './types/HoneColumn';
import { rowArrayToCsvUtf8 } from '@/csv/csvExportUtil';
import { csvUnicodeToRowArray, csvUtf8ToRowArray } from '@/csv/csvImportUtil';
import { csvUnicodeToRowArray, csvUtf8ToRowArray, MAX_FIELDNAME_LENGTH } from '@/csv/csvImportUtil';
import AppException from '@/common/types/AppException';
import Rowset from './types/Rowset';
import { generateColumnNames } from './columnUtil';
Expand Down Expand Up @@ -146,12 +146,21 @@ async function _readClipboardText():Promise<string> {
}
}

async function _readDataTransferText(dataTransfer:DataTransfer):Promise<string> {
try {
const text = await dataTransfer.getData('text/plain');
return text;
} catch(e:any) {
throw new AppException(SheetErrorType.UNEXPECTED_CLIPBOARD_ERROR, e.message);
}
}

function _fileNameToSheetName(filename:string):string {
let dotI = filename.lastIndexOf('.');
return dotI === -1 ? filename : filename.substring(0, dotI);
}

function _firstRowToColumns(firstRow:string[]):HoneColumn[] {
function _rowToColumns(firstRow:string[]):HoneColumn[] {
return firstRow.map(name => ({ name, isWritable:false }));
}

Expand Down Expand Up @@ -192,7 +201,7 @@ export async function importSheetFromCsvFile(fileHandle:FileSystemFileHandle, us
const csvUtf8 = await readFileAsUint8Array(fileHandle);
const sheetName = _fileNameToSheetName(fileHandle.name);
let rows = csvUtf8ToRowArray(csvUtf8, useFirstRowColumnNames);
const columns = _firstRowToColumns(rows[0]);
const columns = _rowToColumns(rows[0]);
rows = rows.slice(1);
return { name:sheetName, columns, rows };
}
Expand All @@ -202,12 +211,32 @@ export async function importSheetFromCsvFile(fileHandle:FileSystemFileHandle, us
export async function importSheetFromClipboard(useFirstRowColumnNames:boolean, sheetName:string):Promise<HoneSheet> {
const text = await _readClipboardText();
let rows = csvUnicodeToRowArray(text, useFirstRowColumnNames);
const columns = _firstRowToColumns(rows[0]);
const columns = _rowToColumns(rows[0]);
rows = rows.slice(1);
if (!rows.length) throw new AppException(SheetErrorType.CLIPBOARD_NO_ROWS, 'No rows found in clipboard data.');
return { name:sheetName, columns, rows };
}

function _doesRowLookLikeColumnNames(row:string[]):boolean {
if (!row.length) return false;
return !row.some(
fieldValue => typeof fieldValue !== 'string'
|| fieldValue.length > MAX_FIELDNAME_LENGTH
|| fieldValue === ''
);
}

export async function importSheetFromClipboardData(clipboardData:DataTransfer, sheetName:string):Promise<HoneSheet> {
const text = await _readDataTransferText(clipboardData);
let rows = csvUnicodeToRowArray(text, false);
if (rows.length === 1) throw new AppException(SheetErrorType.CLIPBOARD_NO_ROWS, 'No rows found in clipboard data.');
const headerRowI = (_doesRowLookLikeColumnNames(rows[1])) ? 1 : 0;
if (rows.length === 2 && headerRowI === 1) throw new AppException(SheetErrorType.CLIPBOARD_NO_ROWS, 'No data rows found in clipboard data.');
const columns = _rowToColumns(rows[headerRowI]);
rows = rows.slice(headerRowI + 1);
return { name:sheetName, columns, rows };
}

export async function exportSheetToClipboard(sheet:HoneSheet, includeHeaders:boolean) {
const fieldNames = getColumnNames(sheet);
const rows = sheet.rows;
Expand Down

0 comments on commit 81170f6

Please sign in to comment.