Skip to content
This repository has been archived by the owner on Oct 18, 2024. It is now read-only.

feat: added json validation and import error modal #25

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,27 @@ async function openFolder() {
});
const { filePaths } = data;
console.log(`filePath`, filePaths[0]);
const files = await fs.readdir(filePaths[0]);
const promises = files.map((file: string) => {
const fileType = file.split('.').pop()?.toLowerCase();
if (fileType === 'json' || fileType === 'geojson') {
console.log('(geo)json found');
return fs.readFile(`${filePaths[0]}${path.sep}${file}`);
}
const files = await fs
.readdir(filePaths[0])
.then((f) => f.map((file) => `${filePaths[0]}${path.sep}${file}`));
const validFiles = files.filter((filename: string) => {
const fileType = filename.split('.').pop()?.toLowerCase();
return fileType === 'json' || fileType === 'geojson';
});

const mapData = await Promise.all(promises);
console.log(`MAIN: ${fileChannel}: `);
console.dir(
mapData.map((data) => (data ? JSON.parse(data.toString()) : null))
);
win?.webContents.send(
fileChannel,
mapData
.map((data) => (data ? JSON.parse(data.toString()) : null))
.filter((data) => !!data)
const filesData = await Promise.all(
validFiles.map((filepath) =>
fs.readFile(filepath).then((content) => ({
filepath,
content: JSON.parse(content.toString())
}))
)
);

// const mapData = await Promise.all(promises);
vinhvn marked this conversation as resolved.
Show resolved Hide resolved
console.log(`MAIN: ${fileChannel}: `);
console.dir(filesData);
win?.webContents.send(fileChannel, filesData);
} catch (error) {
console.error(error);
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@testing-library/user-event": "^12.1.10",
"@types/electron-devtools-installer": "^2.2.0",
"@types/jest": "^26.0.15",
"@types/lodash": "^4.14.172",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
Expand All @@ -29,6 +30,7 @@
"electron-is-dev": "^1.2.0",
"electron-reload": "^1.5.0",
"eslint": "^7.28.0",
"lodash": "^4.17.21",
"mapbox-gl": "^2.3.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
Expand Down
64 changes: 57 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/* eslint @typescript-eslint/no-var-requires: "off" */
import React, { useCallback, useEffect, useState } from 'react';
import { FeatureCollection, Feature } from 'geojson';
import Map from './Map';
import styled, {
createGlobalStyle // TODO: Establish a theme and hook this up to everything
} from 'styled-components';
import Left from './Left';
import Properties from './Properties';
import type { IpcRendererEvent } from 'electron';
import { MapEvent } from 'react-map-gl';
import { partition } from 'lodash';
import { isFeatureCollection } from './util/validateJson';
import Map from './Map';
import Left from './Left';
import Properties from './Properties';
import Modal from './Modal';

const { ipcRenderer } = window.require('electron');

Expand All @@ -18,6 +21,30 @@ const StyledDiv = styled.div`
height: 100vh;
`;

const ErrorHeader = styled.h2`
font-size: 1rem;
line-height: 1.5rem;
margin: 0px;
margin-bottom: 1em;
`;

const ErrorText = styled.p`
font-size: 0.875rem;
line-height: 1.25rem;
margin: 0px;
`;

const ErrorList = styled.ul`
padding-left: 1em;
margin: 0px;
`;

const ErrorItem = styled.li`
font-size: 0.875rem;
line-height: 1.25rem;
margin: 0px;
`;
vinhvn marked this conversation as resolved.
Show resolved Hide resolved

const GlobalStyle = createGlobalStyle`
body {
margin: 0;
Expand All @@ -29,20 +56,32 @@ const carleton = {
longitude: -75.69608
};

interface FileData {
filepath: string;
content: FeatureCollection;
}

const fileChannel = 'file-content';
const App: React.FC = () => {
const [feature, setFeature] = useState<Feature[] | undefined>(undefined);
const [mapData, setData] = useState<FeatureCollection[] | null>(null);
const [errorFiles, setErrorFiles] = useState<string[]>([]);

const displayFeature = useCallback(({ features }: MapEvent) => {
setFeature(features);
}, []);
const [mapData, setData] = useState<FeatureCollection[] | null>(null);

const onFileOpen = useCallback(
(event: IpcRendererEvent, contents: FeatureCollection[]) => {
(event: IpcRendererEvent, contents: FileData[]) => {
console.log(`RENDERER: ${fileChannel}`, contents);
setData(contents);
// TODO: Add file validation
const [validFiles, invalidFiles] = partition(contents, (f) =>
isFeatureCollection(f.content)
);
// if a bad file was found, num of validated files will be less than input contents
if (validFiles.length < contents.length) {
setErrorFiles(invalidFiles.map((f) => f.filepath));
}
setData(validFiles.map((f) => f.content));
},
[]
);
Expand All @@ -67,6 +106,17 @@ const App: React.FC = () => {
displayFeature={displayFeature}
/>
<Properties features={feature} />
{errorFiles.length > 0 ? (
<Modal onClose={() => setErrorFiles([])}>
<ErrorHeader>There was an error importing some files.</ErrorHeader>
<ErrorText>Files that could not be imported:</ErrorText>
<ErrorList>
{errorFiles.map((file, i) => (
<ErrorItem key={`${file}-${i}`}>{file}</ErrorItem>
))}
</ErrorList>
</Modal>
) : null}
</StyledDiv>
</>
);
Expand Down
85 changes: 85 additions & 0 deletions src/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import styled from 'styled-components';

const ModalContainer = styled.div`
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
`;

const ModalBackground = styled.div`
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background-color: #000;
opacity: 0.8;
`;

const ModalContent = styled.div`
position: relative;
display: flex;
flex-direction: column;
background-color: #fff;
opacity: 1;
padding: 2em;
padding-bottom: 3em;
border-radius: 0.25rem;
`;

const ModalHeader = styled.div`
display: flex;
justify-content: flex-end;
width: 100%;
`;

const ModalCloseButton = styled.button`
margin: 0px;
padding: 0px;
cursor: pointer;
background: none;
border: none;
`;
vinhvn marked this conversation as resolved.
Show resolved Hide resolved

interface ModalProps {
onClose: () => void;
children?: React.ReactNode;
}

const Modal: React.FC<ModalProps> = ({ onClose, children }: ModalProps) => {
return (
<ModalContainer>
<ModalBackground onClick={() => onClose()} />
<ModalContent>
<ModalHeader>
<ModalCloseButton onClick={() => onClose()}>
<svg
xmlns='http://www.w3.org/2000/svg'
style={{ width: '1.25rem' }}
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</ModalCloseButton>
</ModalHeader>
{children}
</ModalContent>
</ModalContainer>
);
};

export default Modal;
54 changes: 54 additions & 0 deletions src/util/validateJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint @typescript-eslint/no-explicit-any: "off" */
/* eslint @typescript-eslint/explicit-module-boundary-types: "off" */
import type {
FeatureCollection,
Feature,
GeoJsonProperties,
Geometry
} from 'geojson';

export function isFeatureCollection(object: any): object is FeatureCollection {
vinhvn marked this conversation as resolved.
Show resolved Hide resolved
return (
typeof object !== 'undefined' &&
typeof object === 'object' &&
object.type === 'FeatureCollection' &&
Array.isArray(object.features) &&
object.features.every(isFeature)
);
}

export function isFeature(object: any): object is Feature {
return (
typeof object !== 'undefined' &&
typeof object === 'object' &&
object.type === 'Feature' &&
(typeof object.id === 'string' ||
typeof object.id == 'number' ||
vinhvn marked this conversation as resolved.
Show resolved Hide resolved
typeof object.id === 'undefined') &&
isGeoJsonProperties(object.properties) &&
isGeometry(object.geometry)
);
}

export function isGeoJsonProperties(object: any): object is GeoJsonProperties {
return typeof object === 'object' || object === null;
}

export function isGeometry(object: any): object is Geometry {
return (
typeof object !== 'undefined' &&
typeof object === 'object' &&
(object.type === 'Point' ||
object.type === 'MultiPoint' ||
object.type === 'LineString' ||
object.type === 'MultiLineString' ||
object.type === 'Polygon' ||
object.type === 'MultiPolygon') &&
vinhvn marked this conversation as resolved.
Show resolved Hide resolved
Array.isArray(object.coordinates) &&
(typeof object.coordinates[0] === 'number' ||
(Array.isArray(object.coordinates[0]) &&
(typeof object.coordinates[0][0] === 'number' ||
(Array.isArray(object.coordinates[0][0]) &&
typeof object.coordinates[0][0][0] === 'number'))))
);
vinhvn marked this conversation as resolved.
Show resolved Hide resolved
}
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1946,6 +1946,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=

"@types/lodash@^4.14.172":
version "4.14.172"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a"
integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==

"@types/mapbox-gl@^2.0.3":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-2.1.0.tgz"
Expand Down Expand Up @@ -8075,7 +8080,7 @@ lodash.uniq@^4.5.0:

lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

loglevel@^1.6.8:
Expand Down