diff --git a/src/components/CSVTableEditor.tsx b/src/components/CSVTableEditor.tsx new file mode 100644 index 0000000..c3549f9 --- /dev/null +++ b/src/components/CSVTableEditor.tsx @@ -0,0 +1,228 @@ +import React, { + ChangeEvent, + MouseEvent as RMouseEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import styles from '../css/RMLMappingEditor.module.scss'; + +interface CSVTableEditorProps { + content: string; + onContentChange: (newContent: string) => void; +} + +function CSVTableEditor({ content, onContentChange }: CSVTableEditorProps) { + const table = useMemo(() => stringToTable(content), [content]); + + const [editingCell, setEditingCell] = useState< + [number, number] | undefined + >(); + const [activeInput, setActiveInput] = useState(null); + + const tableRef = useRef(null); + + const handleClickOutside = useCallback( + (e: MouseEvent) => { + const tableDims = tableRef.current?.getBoundingClientRect(); + if ( + tableDims && + (e.clientX > tableDims.x + tableDims.width || + e.clientY > tableDims.y + tableDims.height) + ) { + setEditingCell(undefined); + } + }, + [setEditingCell] + ); + + useEffect(() => { + if (activeInput) activeInput.focus(); + }, [activeInput]); + + useEffect(() => { + onContentChange(table.map((row) => row.join(',')).join('\n')); + }, [onContentChange, table]); + + useEffect(() => { + document.body.addEventListener('click', handleClickOutside); + return () => { + document.body.removeEventListener('click', handleClickOutside); + }; + }, [handleClickOutside]); + + const handleTdClick = useCallback( + (e: RMouseEvent) => { + const [row, col] = [ + parseInt(e.currentTarget.dataset.rowIndex ?? '', 10), + parseInt(e.currentTarget.dataset.colIndex ?? '', 10), + ]; + setEditingCell([row, col]); + }, + [setEditingCell] + ); + + const handleCellValueChange = useCallback( + (e: ChangeEvent) => { + const [row, col] = [ + parseInt(e.currentTarget.dataset.rowIndex ?? '', 10), + parseInt(e.currentTarget.dataset.colIndex ?? '', 10), + ]; + const value = e.currentTarget.value; + const newTable = [...table]; + newTable[row][col] = parseRawValue(value); + onContentChange(tableToString(newTable)); + }, + [onContentChange, table] + ); + + const removeRow = useCallback( + (e: RMouseEvent) => { + const rowIndex = parseInt(e.currentTarget.dataset.rowIndex || '', 10); + + const newTable = [...table]; + newTable.splice(rowIndex, 1); + onContentChange(tableToString(newTable)); + }, + [onContentChange, table] + ); + + const addRow = useCallback(() => { + const columnCount = table[0].length; + const newRow = []; + for (let i = 0; i < columnCount; i++) { + newRow.push(''); + } + onContentChange(tableToString([...table, newRow])); + }, [onContentChange, table]); + + const addColumn = useCallback(() => { + const newTable = [...table]; + for (let i = 0; i < newTable.length; ++i) { + newTable[i].push(''); + } + onContentChange(tableToString(newTable)); + }, [onContentChange, table]); + + return ( + <> + + + + {table[0].map((cellContent, index) => ( + + ))} + + + + {table.slice(1).map((row, rowIndex) => ( + + {row.map((cellContent, colIndex) => { + const correctedRowIndex = rowIndex + 1; + return ( + + ); + })} + + + ))} + +
+ {editingCell && + editingCell[0] === 0 && + editingCell[1] === index ? ( + + ) : ( + parseCellValue(cellContent) + )} +
+ {editingCell && + editingCell[0] === correctedRowIndex && + editingCell[1] === colIndex ? ( + setActiveInput(ref)} + data-row-index={correctedRowIndex} + data-col-index={colIndex} + value={parseCellValue(cellContent)} + onChange={handleCellValueChange} + /> + ) : ( + parseCellValue(cellContent) + )} + + + + +
+
+ + +
+ + ); +} + +export default CSVTableEditor; + +const stringToTable = (content: string) => { + return content.split('\n').map((row) => { + const items = ['']; + row.split('').forEach((char, index) => { + if (char === ',' && row[index - 1] !== '\\') { + items.push(''); + return; + } + items[items.length - 1] += char; + }); + return items; + }); +}; + +const parseCellValue = (cellContent: string) => { + return cellContent.replace(/\\,/g, ','); +}; + +const parseRawValue = (value: string) => { + let parsedValue = ''; + if (value) { + parsedValue = value.replace(/,/g, '\\,'); + } + return parsedValue ?? ''; +}; + +const tableToString = (table: string[][]) => + table.map((row) => row.join(',')).join('\n'); diff --git a/src/components/InputFileEditor.tsx b/src/components/InputFileEditor.tsx new file mode 100644 index 0000000..65cf85d --- /dev/null +++ b/src/components/InputFileEditor.tsx @@ -0,0 +1,41 @@ +import React, { useMemo } from 'react'; +import { InputFile } from '../contexts/InputContext'; +import CodeEditor from './CodeEditor'; +import styles from '../css/RMLMappingEditor.module.scss'; +import CSVTableEditor from './CSVTableEditor'; + +interface InputFileEditorProps { + inputFile: InputFile; + onFileContentsChange: (newContent: string) => void; +} + +function InputFileEditor({ + inputFile, + onFileContentsChange, +}: InputFileEditorProps) { + const fileType = useMemo(() => { + const fileNameParts = inputFile.name.split('.'); + return fileNameParts[fileNameParts.length - 1]; + }, [inputFile]); + + return ( + <> + {fileType === 'csv' && ( + + )} + {fileType !== 'csv' && ( + + )} + + ); +} + +export default InputFileEditor; diff --git a/src/components/InputPanel.tsx b/src/components/InputPanel.tsx index bcb89a2..636ba97 100644 --- a/src/components/InputPanel.tsx +++ b/src/components/InputPanel.tsx @@ -1,10 +1,10 @@ -import { useCallback, useContext, useEffect, useMemo, useState } from "react"; -import InputContext from "../contexts/InputContext"; -import styles from "../css/RMLMappingEditor.module.scss"; -import CodeEditor from "./CodeEditor"; -import { ReactComponent as PlusIcon } from "../images/plus.svg"; -import { ReactComponent as DownArrow } from "../images/down-arrow.svg"; -import { INPUT_TYPES } from '../util/Constants'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import InputContext from '../contexts/InputContext'; +import styles from '../css/RMLMappingEditor.module.scss'; +// import CodeEditor from './CodeEditor'; +import { ReactComponent as PlusIcon } from '../images/plus.svg'; +import { ReactComponent as DownArrow } from '../images/down-arrow.svg'; +import InputFileEditor from './InputFileEditor'; const views = { inputs: "Input Files", @@ -31,17 +31,17 @@ function InputPanel({ addNewInput }: InputPanelProps) { inputFiles.length ); - const inputType = useMemo(() => { - if (selectedInputFile) { - if (selectedInputFile.name.endsWith(".json")) { - return INPUT_TYPES.json; - } else if (selectedInputFile.name.endsWith(".xml")) { - return INPUT_TYPES.xml; - } else if (selectedInputFile.name.endsWith(".csv")) { - return INPUT_TYPES.csv; - } - } - }, [selectedInputFile]); + // const inputType = useMemo(() => { + // if (selectedInputFile) { + // if (selectedInputFile.name.endsWith('.json')) { + // return INPUT_TYPES.json; + // } else if (selectedInputFile.name.endsWith('.xml')) { + // return INPUT_TYPES.xml; + // } else if (selectedInputFile.name.endsWith('.csv')) { + // return INPUT_TYPES.csv; + // } + // } + // }, [selectedInputFile]); const changeToInputView = useCallback(() => setView(views.inputs), [setView]); @@ -81,7 +81,7 @@ function InputPanel({ addNewInput }: InputPanelProps) {