1
1
import * as React from "react" ;
2
2
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" ;
5
5
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" ;
7
8
import classNames from "classnames" ;
8
9
import Fuse from "fuse.js" ;
9
10
import styled from "styled-components" ;
10
11
import { DragDropOpenFileAreaButton } from "./Components/DragDropOpenFileArea.tsx" ;
11
- import { FieldSelectPopup } from "./Components/FieldSelectPopup.tsx" ;
12
12
import { useAntdTheme , useAntdToken } from "./Context/AntdThemeContext.tsx" ;
13
13
import { useNotify } from "./Hooks/Notify.ts" ;
14
14
import { useDynamicDownload } from "./Hooks/UseDynamicDownload.ts" ;
15
+ import { AddEditRowDataPopup } from "./Popups/AddEditRowDataPopup.tsx" ;
16
+ import { FieldSelectPopup } from "./Popups/FieldSelectPopup.tsx" ;
15
17
import { PwaBadge } from "./PwaBadge.tsx" ;
16
18
import type { BaseProps , DictionaryEntry } from "./Types/BaseTypes.ts" ;
17
19
import { getArrayFields } from "./Utilities/ArrayUtils.ts" ;
@@ -20,22 +22,36 @@ import { useLocalStorage } from "./Utilities/UseLocalStorage.tsx";
20
22
type Props = { } & BaseProps ;
21
23
22
24
let App = ( { className } : Props ) => {
25
+ // Local storage
23
26
const [ setItem , , getItem ] = useLocalStorage ( ) ;
24
27
25
- const [ dataSource , setDataSource ] = React . useState < DictionaryEntry [ ] > ( ) ;
28
+ // Antd theme selection design token
26
29
const { token } = useAntdToken ( ) ;
30
+
31
+ // Pure state
32
+ const [ dataSource , setDataSource ] = React . useState < DictionaryEntry [ ] > ( ) ;
27
33
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
28
43
const [ keys , setKeys ] = React . useState < Array < string > > ( getItem < Array < string > > ( "keys" , [ ] ) ) ;
29
44
const [ darkMode , setDarkMode ] = React . useState < boolean > ( getItem < boolean > ( "darkMode" , false ) ) ;
30
45
const [ dictionary , setDictionary ] = React . useState < DictionaryEntry [ ] > ( getItem < DictionaryEntry [ ] > ( "dictionary" , [ ] ) ) ;
31
- const [ selectKeysVisible , setSelectKeysVisible ] = React . useState ( false ) ;
32
- const [ scrollHeight , setScrollHeight ] = React . useState ( 0 ) ;
33
46
47
+ // Ref data not interacting with the UI
34
48
const dictionaryTempRef = React . useRef < DictionaryEntry [ ] | undefined > ( undefined ) ;
35
49
const keysTempRef = React . useRef < Array < string > > ( [ ] ) ;
36
50
51
+ // The area where the notifications go
37
52
const [ contextHolder , notification ] = useNotify ( ) ;
38
53
54
+ // Download the current dictionary
39
55
const downloadLinkClick = useDynamicDownload ( dictionary ) ;
40
56
41
57
const downloadClick = React . useCallback ( ( ) => {
@@ -117,7 +133,14 @@ let App = ({ className }: Props) => {
117
133
118
134
const onSelectKeysClose = React . useCallback (
119
135
( 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 ) {
121
144
setKeys ( fields ?? [ ] ) ;
122
145
setSelectKeysVisible ( false ) ;
123
146
setDictionary ( dictionaryTempRef . current ) ;
@@ -142,7 +165,25 @@ let App = ({ className }: Props) => {
142
165
setSelectKeysVisible ( false ) ;
143
166
}
144
167
} ,
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 ]
146
187
) ;
147
188
148
189
// The Antd table scroll height must be calculated dynamically as it doesn't support max-height
@@ -166,6 +207,38 @@ let App = ({ className }: Props) => {
166
207
} ;
167
208
} , [ onResize ] ) ;
168
209
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
+
169
242
return (
170
243
< div id = "App" className = { classNames ( className , App . name ) } >
171
244
{ contextHolder }
@@ -177,10 +250,12 @@ let App = ({ className }: Props) => {
177
250
id = "app-toolbar-search"
178
251
placeholder = "Search"
179
252
onSearch = { onSearch }
253
+ className = "App-toolbar-search"
180
254
/>
181
255
< Button //
182
256
icon = { < FontAwesomeIcon icon = { faDownload } /> }
183
257
onClick = { downloadClick }
258
+ className = "Toolbar-button"
184
259
/>
185
260
< Switch
186
261
className = "App-toolbar-switch"
@@ -191,6 +266,36 @@ let App = ({ className }: Props) => {
191
266
onChange = { onLightDarkSwitchChangeEventHandler }
192
267
/>
193
268
</ 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 >
194
299
< div id = "table-container" className = "App-table-container" >
195
300
< Table < DictionaryEntry > //
196
301
id = "table-fixed-height"
@@ -201,21 +306,33 @@ let App = ({ className }: Props) => {
201
306
virtual
202
307
scroll = { { y : scrollHeight } }
203
308
rowKey = "id"
309
+ rowSelection = { rowSelection }
204
310
/>
205
311
</ div >
206
312
{ selectKeysVisible && (
207
313
< FieldSelectPopup //
208
314
visible = { selectKeysVisible }
209
- allFields = { keysTempRef . current }
315
+ allFields = { addNew ? defaultNewFields : keysTempRef . current }
210
316
onClose = { onSelectKeysClose }
317
+ addNew = { addNew }
211
318
/>
212
319
) }
213
320
321
+ < AddEditRowDataPopup //
322
+ visible = { addOrEditEntryVisible }
323
+ currentDictionary = { dictionary }
324
+ fields = { keys }
325
+ onClose = { onAddEditRowDataPopupClose }
326
+ editAddEntry = { editEntryNewMode ? undefined : editAddEntry }
327
+ />
328
+
214
329
< PwaBadge />
215
330
</ div >
216
331
) ;
217
332
} ;
218
333
334
+ const defaultNewFields = [ "name" , "value" ] ;
335
+
219
336
App = styled ( App ) `
220
337
display: flex;
221
338
flex-direction: column;
@@ -231,6 +348,7 @@ App = styled(App)`
231
348
display: flex;
232
349
flex-direction: row;
233
350
gap: 10px;
351
+ flex-wrap: wrap;
234
352
}
235
353
.App-toolbar-switch {
236
354
align-self: center;
@@ -239,6 +357,12 @@ App = styled(App)`
239
357
height: 100%;
240
358
overflow: hidden;
241
359
}
360
+ .Toolbar-button {
361
+ min-width: 40px;
362
+ }
363
+ .App-toolbar-search {
364
+ width: 300px;
365
+ }
242
366
` ;
243
367
244
368
export { App } ;
0 commit comments