Skip to content

Commit 9a043b6

Browse files
author
VPKSoft
committed
Add possibility to add and edit new dictionaries.
1 parent 8c6be6d commit 9a043b6

File tree

5 files changed

+268
-13
lines changed

5 files changed

+268
-13
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "json-dictionary-browser",
33
"private": true,
4-
"version": "0.0.0",
4+
"version": "0.0.1",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src/App.tsx

Lines changed: 134 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import * as React from "react";
22
import "./App.css";
3-
import { faMoon, faSun } from "@fortawesome/free-regular-svg-icons";
4-
import { faDownload } from "@fortawesome/free-solid-svg-icons";
3+
import { faEdit, faFile, faMoon, faSun } from "@fortawesome/free-regular-svg-icons";
4+
import { faDownload, faPlus } from "@fortawesome/free-solid-svg-icons";
55
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6-
import { Button, Input, Switch, Table } from "antd";
6+
import { Button, Input, Switch, Table, Tooltip } from "antd";
7+
import type { Key } from "antd/es/table/interface";
78
import classNames from "classnames";
89
import Fuse from "fuse.js";
910
import styled from "styled-components";
1011
import { DragDropOpenFileAreaButton } from "./Components/DragDropOpenFileArea.tsx";
11-
import { FieldSelectPopup } from "./Components/FieldSelectPopup.tsx";
1212
import { useAntdTheme, useAntdToken } from "./Context/AntdThemeContext.tsx";
1313
import { useNotify } from "./Hooks/Notify.ts";
1414
import { useDynamicDownload } from "./Hooks/UseDynamicDownload.ts";
15+
import { AddEditRowDataPopup } from "./Popups/AddEditRowDataPopup.tsx";
16+
import { FieldSelectPopup } from "./Popups/FieldSelectPopup.tsx";
1517
import { PwaBadge } from "./PwaBadge.tsx";
1618
import type { BaseProps, DictionaryEntry } from "./Types/BaseTypes.ts";
1719
import { getArrayFields } from "./Utilities/ArrayUtils.ts";
@@ -20,22 +22,36 @@ import { useLocalStorage } from "./Utilities/UseLocalStorage.tsx";
2022
type Props = {} & BaseProps;
2123

2224
let App = ({ className }: Props) => {
25+
// Local storage
2326
const [setItem, , getItem] = useLocalStorage();
2427

25-
const [dataSource, setDataSource] = React.useState<DictionaryEntry[]>();
28+
// Antd theme selection design token
2629
const { token } = useAntdToken();
30+
31+
// Pure state
32+
const [dataSource, setDataSource] = React.useState<DictionaryEntry[]>();
2733
const { setTheme, updateBackground } = useAntdTheme();
34+
const [selectKeysVisible, setSelectKeysVisible] = React.useState(false);
35+
const [scrollHeight, setScrollHeight] = React.useState(0);
36+
const [addNew, setAddNew] = React.useState(false);
37+
const [addOrEditEntryVisible, setAddOrEditEntryVisible] = React.useState(false);
38+
const [editAddEntry, setEditAddEntry] = React.useState<DictionaryEntry | undefined>(undefined);
39+
const [editEntryNewMode, setEditEntryNewMode] = React.useState(true);
40+
const [selectedRowKeys, setSelectedRowKeys] = React.useState<Array<Key>>([]);
41+
42+
// Persistent state
2843
const [keys, setKeys] = React.useState<Array<string>>(getItem<Array<string>>("keys", []));
2944
const [darkMode, setDarkMode] = React.useState<boolean>(getItem<boolean>("darkMode", false));
3045
const [dictionary, setDictionary] = React.useState<DictionaryEntry[]>(getItem<DictionaryEntry[]>("dictionary", []));
31-
const [selectKeysVisible, setSelectKeysVisible] = React.useState(false);
32-
const [scrollHeight, setScrollHeight] = React.useState(0);
3346

47+
// Ref data not interacting with the UI
3448
const dictionaryTempRef = React.useRef<DictionaryEntry[] | undefined>(undefined);
3549
const keysTempRef = React.useRef<Array<string>>([]);
3650

51+
// The area where the notifications go
3752
const [contextHolder, notification] = useNotify();
3853

54+
// Download the current dictionary
3955
const downloadLinkClick = useDynamicDownload(dictionary);
4056

4157
const downloadClick = React.useCallback(() => {
@@ -117,7 +133,14 @@ let App = ({ className }: Props) => {
117133

118134
const onSelectKeysClose = React.useCallback(
119135
(accept: boolean, fields?: Array<string>) => {
120-
if (accept && dictionaryTempRef.current) {
136+
if (accept && addNew) {
137+
dictionaryTempRef.current = undefined;
138+
setKeys(fields ?? []);
139+
setSelectKeysVisible(false);
140+
setDictionary([]);
141+
setItem("dictionary", []);
142+
setItem("keys", fields ?? []);
143+
} else if (accept && dictionaryTempRef.current) {
121144
setKeys(fields ?? []);
122145
setSelectKeysVisible(false);
123146
setDictionary(dictionaryTempRef.current);
@@ -142,7 +165,25 @@ let App = ({ className }: Props) => {
142165
setSelectKeysVisible(false);
143166
}
144167
},
145-
[setItem]
168+
[setItem, addNew]
169+
);
170+
171+
const onAddEditRowDataPopupClose = React.useCallback(
172+
(accept: boolean, data?: DictionaryEntry) => {
173+
if (accept && data) {
174+
let newDictionary = [...dictionary];
175+
if (editEntryNewMode) {
176+
newDictionary.push(data);
177+
} else if (editAddEntry && !editEntryNewMode) {
178+
const index = newDictionary.findIndex(f => f.id === editAddEntry.id);
179+
newDictionary[index] = data;
180+
}
181+
setDictionary(newDictionary);
182+
setItem("dictionary", newDictionary);
183+
}
184+
setAddOrEditEntryVisible(false);
185+
},
186+
[setItem, dictionary, editAddEntry, editEntryNewMode]
146187
);
147188

148189
// The Antd table scroll height must be calculated dynamically as it doesn't support max-height
@@ -166,6 +207,38 @@ let App = ({ className }: Props) => {
166207
};
167208
}, [onResize]);
168209

210+
const addNewClick = React.useCallback(() => {
211+
setAddNew(true);
212+
setSelectKeysVisible(true);
213+
}, []);
214+
215+
const addClick = React.useCallback(() => {
216+
setEditEntryNewMode(true);
217+
setAddOrEditEntryVisible(true);
218+
}, []);
219+
220+
const editClick = React.useCallback(() => {
221+
if (selectedRowKeys.length > 0) {
222+
const lastSelectedKey = selectedRowKeys[selectedRowKeys.length - 1];
223+
setEditAddEntry(dictionary.find(item => item.id === lastSelectedKey));
224+
225+
setEditEntryNewMode(false);
226+
setAddOrEditEntryVisible(true);
227+
}
228+
}, [selectedRowKeys, dictionary]);
229+
230+
const onSelectionChange = React.useCallback((selectedRowKeys: Array<Key>) => {
231+
setSelectedRowKeys(selectedRowKeys);
232+
}, []);
233+
234+
const rowSelection = React.useMemo(() => {
235+
return {
236+
onChange: onSelectionChange,
237+
columnWidth: 60,
238+
columnTitle: "Select",
239+
};
240+
}, [onSelectionChange]);
241+
169242
return (
170243
<div id="App" className={classNames(className, App.name)}>
171244
{contextHolder}
@@ -177,10 +250,12 @@ let App = ({ className }: Props) => {
177250
id="app-toolbar-search"
178251
placeholder="Search"
179252
onSearch={onSearch}
253+
className="App-toolbar-search"
180254
/>
181255
<Button //
182256
icon={<FontAwesomeIcon icon={faDownload} />}
183257
onClick={downloadClick}
258+
className="Toolbar-button"
184259
/>
185260
<Switch
186261
className="App-toolbar-switch"
@@ -191,6 +266,36 @@ let App = ({ className }: Props) => {
191266
onChange={onLightDarkSwitchChangeEventHandler}
192267
/>
193268
</div>
269+
<div className="App-toolbar">
270+
<Tooltip title="New dictionary">
271+
<Button //
272+
icon={<FontAwesomeIcon icon={faFile} />}
273+
onClick={addNewClick}
274+
className="Toolbar-button"
275+
>
276+
New dictionary
277+
</Button>
278+
</Tooltip>
279+
<Tooltip title="Add new entry">
280+
<Button //
281+
icon={<FontAwesomeIcon icon={faPlus} />}
282+
onClick={addClick}
283+
className="Toolbar-button"
284+
>
285+
Add new entry
286+
</Button>
287+
</Tooltip>
288+
<Tooltip title="Edit selected entry">
289+
<Button //
290+
icon={<FontAwesomeIcon icon={faEdit} />}
291+
onClick={editClick}
292+
className="Toolbar-button"
293+
disabled={selectedRowKeys.length === 0}
294+
>
295+
Edit selected entry
296+
</Button>
297+
</Tooltip>
298+
</div>
194299
<div id="table-container" className="App-table-container">
195300
<Table<DictionaryEntry> //
196301
id="table-fixed-height"
@@ -201,21 +306,33 @@ let App = ({ className }: Props) => {
201306
virtual
202307
scroll={{ y: scrollHeight }}
203308
rowKey="id"
309+
rowSelection={rowSelection}
204310
/>
205311
</div>
206312
{selectKeysVisible && (
207313
<FieldSelectPopup //
208314
visible={selectKeysVisible}
209-
allFields={keysTempRef.current}
315+
allFields={addNew ? defaultNewFields : keysTempRef.current}
210316
onClose={onSelectKeysClose}
317+
addNew={addNew}
211318
/>
212319
)}
213320

321+
<AddEditRowDataPopup //
322+
visible={addOrEditEntryVisible}
323+
currentDictionary={dictionary}
324+
fields={keys}
325+
onClose={onAddEditRowDataPopupClose}
326+
editAddEntry={editEntryNewMode ? undefined : editAddEntry}
327+
/>
328+
214329
<PwaBadge />
215330
</div>
216331
);
217332
};
218333

334+
const defaultNewFields = ["name", "value"];
335+
219336
App = styled(App)`
220337
display: flex;
221338
flex-direction: column;
@@ -231,6 +348,7 @@ App = styled(App)`
231348
display: flex;
232349
flex-direction: row;
233350
gap: 10px;
351+
flex-wrap: wrap;
234352
}
235353
.App-toolbar-switch {
236354
align-self: center;
@@ -239,6 +357,12 @@ App = styled(App)`
239357
height: 100%;
240358
overflow: hidden;
241359
}
360+
.Toolbar-button {
361+
min-width: 40px;
362+
}
363+
.App-toolbar-search {
364+
width: 300px;
365+
}
242366
`;
243367

244368
export { App };

src/Popups/AddEditRowDataPopup.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Input, Modal } from "antd";
2+
import classNames from "classnames";
3+
import * as React from "react";
4+
import styled from "styled-components";
5+
import type { DictionaryEntry } from "../Types/BaseTypes";
6+
import { pascalCase } from "../Utilities/Misc";
7+
8+
/**
9+
* The props for the {@link PreferencesPopup} component.
10+
*/
11+
type AddEditRowDataPopupProps = {
12+
/** The class name for the component. */
13+
className?: string;
14+
/** An array of field names in the data entry. */
15+
fields: Array<string>;
16+
/** A value indicating whether this popup is visible. */
17+
visible: boolean;
18+
/** The data entry to edit or add. */
19+
editAddEntry: DictionaryEntry | undefined;
20+
/** The current dictionary. */
21+
currentDictionary: DictionaryEntry[];
22+
/** A function to close this popup. */
23+
onClose: (accept: boolean, entry?: DictionaryEntry) => void;
24+
};
25+
26+
let AddEditRowDataPopup = ({
27+
className, //
28+
fields,
29+
visible,
30+
editAddEntry,
31+
currentDictionary,
32+
onClose,
33+
}: AddEditRowDataPopupProps) => {
34+
const [editAddEntryInternal, setEditAddEntryInternal] = React.useState<DictionaryEntry | undefined>();
35+
36+
React.useEffect(() => {
37+
if (editAddEntry) {
38+
setEditAddEntryInternal(editAddEntry);
39+
} else {
40+
const newEntry = createEmptyEntry(fields, currentDictionary);
41+
setEditAddEntryInternal(newEntry);
42+
}
43+
}, [editAddEntry, fields, currentDictionary]);
44+
45+
const onOkClick = React.useCallback(() => {
46+
onClose(true, editAddEntryInternal);
47+
}, [onClose, editAddEntryInternal]);
48+
49+
const onCancel = React.useCallback(() => {
50+
onClose(false);
51+
}, [onClose]);
52+
53+
const editValues = React.useMemo(() => {
54+
if (!editAddEntryInternal) {
55+
return null;
56+
}
57+
58+
const result = fields.map(f => (
59+
<tr key={f}>
60+
<td>{pascalCase(f)}</td>
61+
<td>
62+
<Input
63+
type="text"
64+
value={editAddEntryInternal?.[f] || ""}
65+
onChange={e => setEditAddEntryInternal({ ...editAddEntryInternal, [f]: e.target.value })}
66+
/>
67+
</td>
68+
</tr>
69+
));
70+
return result;
71+
}, [fields, editAddEntryInternal]);
72+
73+
return (
74+
<Modal //
75+
title="Select fields"
76+
open={visible}
77+
width={600}
78+
centered
79+
onCancel={onCancel}
80+
onOk={onOkClick}
81+
keyboard
82+
maskClosable={false}
83+
okButtonProps={{ disabled: fields.length === 0 }}
84+
closable={false}
85+
>
86+
<table className={classNames(AddEditRowDataPopup.name, className)}>
87+
<tbody>{editValues}</tbody>
88+
</table>
89+
</Modal>
90+
);
91+
};
92+
93+
const createEmptyEntry = (fields: Array<string>, currentDictionary: Array<DictionaryEntry>) => {
94+
const result = {} as DictionaryEntry;
95+
for (const field of fields) {
96+
result[field] = "";
97+
}
98+
99+
let max = currentDictionary.map(d => d.id).reduce((a, b) => Math.max(a, b), 0) + 1;
100+
101+
result["id"] = max;
102+
return result;
103+
};
104+
105+
AddEditRowDataPopup = styled(AddEditRowDataPopup)`
106+
display: flex;
107+
flex-direction: column;
108+
height: 100%;
109+
.Select-width {
110+
width: 450px;
111+
}
112+
`;
113+
114+
export { AddEditRowDataPopup };

0 commit comments

Comments
 (0)