diff --git a/src/tools/graph/MappingUI.tsx b/src/tools/graph/MappingUI.tsx index b4501e8..1f2359a 100644 --- a/src/tools/graph/MappingUI.tsx +++ b/src/tools/graph/MappingUI.tsx @@ -2,7 +2,7 @@ import { DndWrapper } from '@ir-engine/editor/src/components/dnd/DndWrapper' import { ItemTypes } from '@ir-engine/editor/src/constants/AssetTypes' import { getMutableState, NO_PROXY, useHookstate } from '@ir-engine/hyperflux' import { Button, Input } from '@ir-engine/ui' -import React, { useEffect } from 'react' +import React, { useCallback, useEffect } from 'react' import { useDrop } from 'react-dnd' import { HiChevronLeft, HiChevronRight } from 'react-icons/hi' import { useSearchParam } from '../../utils/useSearchParam' @@ -81,21 +81,85 @@ export const MappingUI = () => { ) } +/** + * Takes a raw CSV string and converts it to a JavaScript object. + * @param {string} text The raw CSV string. + * @param {string[]} headers An optional array of headers to use. If none are + * given, they are pulled from the first line of `text`. + * @param {string} quoteChar A character to use as the encapsulating character. + * @param {string} delimiter A character to use between columns. + * @returns {object[]} An array of JavaScript objects containing headers as keys + * and row entries as values. + */ +function csvToJson(text: string, headers = undefined, quoteChar = '"', delimiter = ',') { + const regex = new RegExp(`\\s*(${quoteChar})?(.*?)\\1\\s*(?:${delimiter}|$)`, 'gs') + + const match = (line: string) => + [...line.matchAll(regex)] + .map((m) => m[2]) // we only want the second capture group + .slice(0, -1) // cut off blank match at the end + + const lines = text.split('\n') + const linesMatched = lines.map((line) => match(line)) + // the header line is the line with the most non empty cells, the lines above can be discarded + const headerline = linesMatched.findIndex( + (line) => + line.filter((cell) => cell.length > 0).length === + Math.max(...linesMatched.map((line) => line.filter((cell) => cell.length > 0).length)) + ) + const heads = headers ?? linesMatched[headerline] + + return linesMatched.slice(headerline + 1, linesMatched.length - 1).map((lineMatched) => { + return lineMatched.reduce((acc, cur, i) => { + // Attempt to parse as a number; replace blank matches with `null` + const val = cur.length <= 0 ? null : Number(cur) || cur + const key = heads[i] ?? `extra_${i}` + return { ...acc, [key]: val } + }, {}) + }) +} + +const parseResponse = async (response: Response, fileType: string) => { + if (fileType === 'json') { + return response.json() + } else if (fileType === 'csv') { + const text = await response.text() + return csvToJson(text) + } + throw new Error('Unknown file type') +} + +const FileTypeOptions = [ + { + label: 'JSON', + value: 'json' + }, + { + label: 'CSV', + value: 'csv' + } +] + const InputData = (props: { onNewData: (data: { schema: JSONSchema; data: unknown }) => void }) => { const rawData = useHookstate<{ schema: JSONSchema; data: unknown } | null>(null) const selectedURL = useHookstate(new URLSearchParams(window.location.search).get('url') || '') + const fileType = useHookstate(new URLSearchParams(window.location.search).get('fileType') || FileTypeOptions[0].value) const loadingData = useHookstate(false) - useEffect(() => { + const fetchData = useCallback(() => { if (!selectedURL.value) return const abortController = new AbortController() loadingData.set(true) fetch(selectedURL.value) .then((response) => { if (abortController.signal.aborted) return - response - .json() + if (!response.ok) { + console.warn('Failed to fetch data') + loadingData.set(false) + return + } + parseResponse(response, fileType.value) .then((data) => { if (abortController.signal.aborted) return const schema = generateJsonSchema(data) @@ -116,8 +180,9 @@ const InputData = (props: { onNewData: (data: { schema: JSONSchema; data: unknow abortController.abort() loadingData.set(false) } - }, [selectedURL]) + }, []) + useSearchParam('fileType', fileType.value) useSearchParam('url', selectedURL.value.startsWith('blob://') ? '' : selectedURL.value) useEffect(() => { @@ -137,10 +202,24 @@ const InputData = (props: { onNewData: (data: { schema: JSONSchema; data: unknow + {loadingData.value ? (
Loading...
) : ( - )}