Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable open and save the family data file, add diagram preview when editing yaml #17

Merged
merged 9 commits into from
Aug 6, 2023
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"bootstrap": "^5.2.3",
"file-saver": "^2.0.5",
"gojs": "^2.3.6",
"lodash": "^4.17.21",
"react": "^18.2.0",
Expand Down Expand Up @@ -40,6 +41,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/file-saver": "^2.0.5",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.195",
"@types/node": "^16.18.25",
Expand Down
59 changes: 56 additions & 3 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap';
import { useMemo, useState } from 'react';
import { stringify } from 'yaml';
import { parse, stringify } from 'yaml';
import { saveAs } from 'file-saver';
import { Person } from '../family.interface';
import AppContext from './AppContext';
import Family from './Family';
Expand All @@ -9,7 +10,12 @@ import ModalAddSpouse from './ModalAddSpouse';
import ModalAddTree from './ModalAddTree';
import ModalDeletePerson from './ModalDeletePerson';
import ModalEditYaml from './ModalEditYaml';
import { deletePerson, enrichTreeData, treesToRecord } from '../family.util';
import {
deletePerson,
enrichTreeData,
treesToRecord,
unrichTreeData,
} from '../family.util';
import { useCache } from '../useCache';

interface AppProps {
Expand Down Expand Up @@ -51,7 +57,7 @@ function App(props: AppProps) {
const [showModalEditYaml, setShowModalEditYaml] = useState(false);
const toggleModalEditYaml = () => setShowModalEditYaml(!showModalEditYaml);
const openModalEditYaml = () => {
setTreeYaml(stringify(trees));
setTreeYaml(stringify(unrichTreeData(trees)));
setShowModalEditYaml(true);
};

Expand All @@ -63,6 +69,39 @@ function App(props: AppProps) {
setShowModalDeletePerson(true);
};

const handleSave = () => {
try {
const unrichedTrees = unrichTreeData(trees);
const treeYaml = stringify(unrichedTrees as {});
const blob = new Blob([treeYaml], { type: 'text/yaml;charset=utf-8' });
saveAs(blob, 'family_data.yaml');
} catch (error) {
console.error('Error saving YAML file:', error);
}
};

const handleLoad = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
try {
const treeYaml = reader.result as string;
// assert valid treeYaml
const rawFamilyData = parse(treeYaml);
const trees = enrichTreeData(rawFamilyData?.trees, rawFamilyData?.people);

const unrichedTrees = unrichTreeData(trees);
setTreeYaml(stringify(unrichedTrees as {}));
setShowModalEditYaml(true);
} catch (error) {
console.error('Error loading YAML file:', error);
}
};
reader.readAsText(file);
}
};

const treeMap = useMemo(() => treesToRecord(trees), [trees]);

const upsertPerson = (person: Person) => {
Expand Down Expand Up @@ -146,6 +185,20 @@ function App(props: AppProps) {
Delete person
</Button>
</FormGroup>
<FormGroup>
<Button size="sm" tag="label">
Import
<Input
type="file"
className="d-none"
accept=".yaml, .yml"
onChange={handleLoad}
/>
</Button>{' '}
<Button size="sm" onClick={handleSave}>
Export
</Button>
</FormGroup>
</Form>
</Container>

Expand Down
40 changes: 28 additions & 12 deletions src/components/ModalEditYaml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from 'react';
import {
Button,
Card,
CardBody,
FormFeedback,
FormGroup,
Input,
Expand All @@ -16,9 +18,11 @@ import {
ModalFooter,
ModalHeader,
} from 'reactstrap';
import { Person } from '../family.interface';
import { enrichTreeData } from '../family.util';
import { parse } from 'yaml';
import AppContext from './AppContext';
import FamilyDiagram from './FamilyDiagram';

interface ModalDeletePersonProps {
treeYaml: string;
Expand All @@ -30,36 +34,40 @@ interface ModalDeletePersonProps {
const MIN_ROW = 3;
const MAX_ROW = 20;

function ModalDeletePerson({
function ModalEditYaml({
treeYaml,
setTreeYaml,
isOpen,
toggle,
}: ModalDeletePersonProps) {
const { setTreesValue } = useContext(AppContext);
const [yamlError, setYamlError] = useState('');
const [trees, setTrees] = useState([] as Person[]);
const deferredTree = useDeferredValue(trees);
const deferredTreeYaml = useDeferredValue(treeYaml);
const loading = deferredTreeYaml !== treeYaml;

const validForm = treeYaml && !yamlError;

const treesFromYaml = (yaml: string) => {
const rawFamilyData = parse(yaml);
return enrichTreeData(rawFamilyData?.trees, rawFamilyData?.people);
};

useEffect(() => {
try {
// test parsing the yaml
enrichTreeData(parse(deferredTreeYaml), []);
if (yamlError !== '') {
setYamlError('');
}
const trees = treesFromYaml(deferredTreeYaml);
setTrees(trees);
setYamlError('');
} catch (e: any) {
setYamlError(e.message);
}
}, [deferredTreeYaml, yamlError]);
}, [deferredTreeYaml]);

const rows = useMemo(() => {
const lines = treeYaml.split('\n').length;
if (lines < MIN_ROW) return MIN_ROW;
if (lines > MAX_ROW) return MAX_ROW;
return lines;
return Math.min(Math.max(lines, MIN_ROW), MAX_ROW);
}, [treeYaml]);

const handleTreeYamlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -70,7 +78,7 @@ function ModalDeletePerson({
const handleSubmit = () => {
if (loading || !validForm) return;

const trees = parse(treeYaml);
const trees = treesFromYaml(treeYaml);
setTreesValue(trees);
toggle();
};
Expand All @@ -80,7 +88,15 @@ function ModalDeletePerson({
<ModalHeader toggle={toggle}>Edit tree</ModalHeader>
<ModalBody>
<FormGroup>
<Label for="edit-tree">Tree</Label>
<Label for="tree-preview">Tree preview</Label>
<Card for="tree-preview" outline color={validForm ? '' : 'danger'}>
<CardBody style={{ opacity: validForm ? 1 : 0.3 }}>
<FamilyDiagram trees={deferredTree} />
</CardBody>
</Card>
</FormGroup>
<FormGroup>
<Label for="edit-tree">Edit tree</Label>
<Input
type="textarea"
id="edit-tree"
Expand All @@ -106,4 +122,4 @@ function ModalDeletePerson({
);
}

export default ModalDeletePerson;
export default ModalEditYaml;
Loading
Loading