@etsoo/react 1.3.99 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
6
6
  /**
7
7
  * Item renderer
8
8
  */
9
- children: (item: D, index: number, deleteItem: (index: number) => void) => React.ReactNode;
9
+ children: (item: D, index: number, deleteItem: (index: number) => void, editItem: (newItem: D, index: number) => boolean) => React.ReactNode;
10
10
  /**
11
11
  * List container component
12
12
  * https://javascript.plainenglish.io/building-a-polymorphic-component-in-react-and-typescript-d9f236950af4
@@ -32,6 +32,10 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
32
32
  * Load data
33
33
  */
34
34
  loadData: (name: string) => PromiseLike<D[]>;
35
+ /**
36
+ * Data change handler
37
+ */
38
+ onChange?: (items: D[]) => void;
35
39
  /**
36
40
  * Top and bottom sides renderer
37
41
  */
package/lib/mu/DnDList.js CHANGED
@@ -9,9 +9,16 @@ import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
9
9
  */
10
10
  export function DnDList(props) {
11
11
  // Destruct
12
- const { children, Component = 'div', componentProps, getItemStyle = (_isDragging) => ({}), getListStyle = () => undefined, labelField, loadData, name, sideRenderer } = props;
12
+ const { children, Component = 'div', componentProps, getItemStyle = (_isDragging) => ({}), getListStyle = () => undefined, labelField, loadData, name, onChange, sideRenderer } = props;
13
13
  // State
14
14
  const [items, setItems] = React.useState([]);
15
+ const changeItems = (items) => {
16
+ // Possible to alter items with the handler
17
+ if (onChange)
18
+ onChange(items);
19
+ // Update state
20
+ setItems(items);
21
+ };
15
22
  // Drag end handler
16
23
  const onDragEnd = (result) => {
17
24
  // Dropped outside the list
@@ -25,18 +32,32 @@ export function DnDList(props) {
25
32
  // Insert to the destination index
26
33
  newItems.splice(result.destination.index, 0, removed);
27
34
  // Update the state
28
- setItems(newItems);
35
+ changeItems(newItems);
29
36
  };
30
37
  // Add item
31
38
  const addItem = (newItem) => {
32
39
  // Existence check
33
- if (items.some((item) => item[labelField] == newItem[labelField])) {
40
+ if (items.some((item) => item[labelField] === newItem[labelField])) {
34
41
  return false;
35
42
  }
36
43
  // Clone
37
44
  const newItems = [newItem, ...items];
38
45
  // Update the state
39
- setItems(newItems);
46
+ changeItems(newItems);
47
+ return true;
48
+ };
49
+ // Edit item
50
+ const editItem = (newItem, index) => {
51
+ // Existence check
52
+ if (items.some((item) => item[labelField] === newItem[labelField])) {
53
+ return false;
54
+ }
55
+ // Clone
56
+ const newItems = [...items];
57
+ // Remove the item
58
+ newItems.splice(index, 1, newItem);
59
+ // Update the state
60
+ changeItems(newItems);
40
61
  return true;
41
62
  };
42
63
  // Add items
@@ -52,7 +73,7 @@ export function DnDList(props) {
52
73
  newItems.push(newItem);
53
74
  });
54
75
  // Update the state
55
- setItems(newItems);
76
+ changeItems(newItems);
56
77
  return newItems.length - items.length;
57
78
  };
58
79
  // Delete item
@@ -62,9 +83,10 @@ export function DnDList(props) {
62
83
  // Remove the item
63
84
  newItems.splice(index, 1);
64
85
  // Update the state
65
- setItems(newItems);
86
+ changeItems(newItems);
66
87
  };
67
88
  React.useEffect(() => {
89
+ // Load data
68
90
  loadData(name).then((items) => setItems(items));
69
91
  }, [name]);
70
92
  // Layout
@@ -83,7 +105,7 @@ export function DnDList(props) {
83
105
  ...provided
84
106
  .draggableProps
85
107
  .style
86
- } }, children(item, index, deleteItem)))));
108
+ } }, children(item, index, deleteItem, editItem)))));
87
109
  }),
88
110
  provided.placeholder))))),
89
111
  sideRenderer && sideRenderer(false, addItem, addItems)));
@@ -1,5 +1,5 @@
1
1
  import { NotificationAlign, NotificationMessageType, NotificationType } from '@etsoo/notificationbase';
2
- import { Alert, AlertTitle, Backdrop, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fade, Slider, Snackbar, styled, Switch, TextField } from '@mui/material';
2
+ import { Alert, AlertTitle, Backdrop, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fade, Slider, Snackbar, styled, Switch, TextField, Typography } from '@mui/material';
3
3
  import { Error, Info, Help, Warning, Done } from '@mui/icons-material';
4
4
  import React from 'react';
5
5
  import { Labels } from '../app/Labels';
@@ -120,14 +120,21 @@ export class NotificationMU extends NotificationReact {
120
120
  const labels = Labels.NotificationMU;
121
121
  const title = (_a = this.title) !== null && _a !== void 0 ? _a : labels.promptTitle;
122
122
  const { cancelLabel = labels.promptCancel, okLabel = labels.promptOK, inputs, type, fullScreen, fullWidth = true, maxWidth, primaryButton, ...rest } = (_b = this.inputProps) !== null && _b !== void 0 ? _b : {};
123
+ const inputRef = React.createRef();
124
+ const errorRef = React.createRef();
125
+ const setError = (error) => {
126
+ if (errorRef.current == null)
127
+ return;
128
+ errorRef.current.innerText = error !== null && error !== void 0 ? error : '';
129
+ };
123
130
  const handleSubmit = async (event) => {
124
131
  // Result
125
132
  let result = undefined;
133
+ const input = inputRef.current;
126
134
  if (this.onReturn) {
127
135
  // Inputs case, no HTMLForm set to value, set the current form
128
136
  if (inputs && value == null)
129
137
  value = event.currentTarget.form;
130
- const input = inputRef.current;
131
138
  if (input) {
132
139
  if (type === 'date') {
133
140
  const dateValue = input.valueAsDate;
@@ -165,12 +172,18 @@ export class NotificationMU extends NotificationReact {
165
172
  // Get the value
166
173
  // returns false to prevent default dismiss
167
174
  const v = await result;
168
- if (v === false)
175
+ if (v === false) {
176
+ input === null || input === void 0 ? void 0 : input.focus();
169
177
  return;
178
+ }
179
+ if (typeof v === 'string') {
180
+ setError(v);
181
+ input === null || input === void 0 ? void 0 : input.focus();
182
+ return;
183
+ }
170
184
  this.dismiss();
171
185
  };
172
186
  let localInputs;
173
- let inputRef = React.createRef();
174
187
  let value = undefined;
175
188
  if (inputs == null) {
176
189
  if (type === 'switch') {
@@ -180,7 +193,7 @@ export class NotificationMU extends NotificationReact {
180
193
  localInputs = React.createElement(Slider, { onChange: (_e, v) => (value = v) });
181
194
  }
182
195
  else {
183
- localInputs = (React.createElement(TextField, { inputRef: inputRef, autoFocus: true, margin: "dense", fullWidth: true, type: type, required: true, ...rest }));
196
+ localInputs = (React.createElement(TextField, { inputRef: inputRef, onChange: () => setError(undefined), autoFocus: true, margin: "dense", fullWidth: true, type: type, required: true, ...rest }));
184
197
  }
185
198
  }
186
199
  else {
@@ -193,7 +206,8 @@ export class NotificationMU extends NotificationReact {
193
206
  React.createElement("span", { className: "dialogTitle" }, title)),
194
207
  React.createElement(DialogContent, null,
195
208
  React.createElement(DialogContentText, null, this.content),
196
- localInputs),
209
+ localInputs,
210
+ React.createElement(Typography, { variant: "caption", display: "block", ref: errorRef, color: (theme) => theme.palette.error.main })),
197
211
  React.createElement(DialogActions, null,
198
212
  React.createElement(Button, { color: "secondary", onClick: () => {
199
213
  if (this.onReturn)
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { Labels } from '../../app/Labels';
4
4
  import { MUGlobal } from '../MUGlobal';
5
5
  import { CommonPage, CommonPageScrollContainer } from './CommonPage';
6
- import EditIcon from '@mui/icons-material/Edit';
6
+ import SaveIcon from '@mui/icons-material/Save';
7
7
  /**
8
8
  * Edit page
9
9
  * @param props Props
@@ -21,5 +21,5 @@ export function EditPage(props) {
21
21
  bottom: (theme) => MUGlobal.updateWithTheme(paddings, theme.spacing),
22
22
  paddingTop: paddings
23
23
  } },
24
- React.createElement(Button, { variant: "contained", type: "submit", fullWidth: true, startIcon: React.createElement(EditIcon, null) }, labels.save)))));
24
+ React.createElement(Button, { variant: "contained", type: "submit", fullWidth: true, startIcon: React.createElement(SaveIcon, null) }, labels.save)))));
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/react",
3
- "version": "1.3.99",
3
+ "version": "1.4.3",
4
4
  "description": "TypeScript ReactJs framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -50,8 +50,8 @@
50
50
  "@emotion/react": "^11.7.1",
51
51
  "@emotion/style": "^0.8.0",
52
52
  "@emotion/styled": "^11.6.0",
53
- "@etsoo/appscript": "^1.2.11",
54
- "@etsoo/notificationbase": "^1.0.95",
53
+ "@etsoo/appscript": "^1.2.15",
54
+ "@etsoo/notificationbase": "^1.1.0",
55
55
  "@etsoo/shared": "^1.0.97",
56
56
  "@mui/icons-material": "^5.2.5",
57
57
  "@mui/material": "^5.2.6",
@@ -76,12 +76,12 @@
76
76
  "react-window": "^1.8.6"
77
77
  },
78
78
  "devDependencies": {
79
- "@babel/cli": "^7.16.0",
80
- "@babel/core": "^7.16.5",
81
- "@babel/plugin-transform-runtime": "^7.16.5",
82
- "@babel/preset-env": "^7.16.5",
83
- "@babel/runtime-corejs3": "^7.16.5",
84
- "@types/jest": "^27.0.3",
79
+ "@babel/cli": "^7.16.7",
80
+ "@babel/core": "^7.16.7",
81
+ "@babel/plugin-transform-runtime": "^7.16.7",
82
+ "@babel/preset-env": "^7.16.7",
83
+ "@babel/runtime-corejs3": "^7.16.7",
84
+ "@types/jest": "^27.4.0",
85
85
  "@types/react-test-renderer": "^17.0.1",
86
86
  "@typescript-eslint/eslint-plugin": "^5.8.1",
87
87
  "@typescript-eslint/parser": "^5.8.1",
@@ -17,7 +17,8 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
17
17
  children: (
18
18
  item: D,
19
19
  index: number,
20
- deleteItem: (index: number) => void
20
+ deleteItem: (index: number) => void,
21
+ editItem: (newItem: D, index: number) => boolean
21
22
  ) => React.ReactNode;
22
23
 
23
24
  /**
@@ -51,6 +52,11 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
51
52
  */
52
53
  loadData: (name: string) => PromiseLike<D[]>;
53
54
 
55
+ /**
56
+ * Data change handler
57
+ */
58
+ onChange?: (items: D[]) => void;
59
+
54
60
  /**
55
61
  * Top and bottom sides renderer
56
62
  */
@@ -86,12 +92,21 @@ export function DnDList<
86
92
  labelField,
87
93
  loadData,
88
94
  name,
95
+ onChange,
89
96
  sideRenderer
90
97
  } = props;
91
98
 
92
99
  // State
93
100
  const [items, setItems] = React.useState<D[]>([]);
94
101
 
102
+ const changeItems = (items: D[]) => {
103
+ // Possible to alter items with the handler
104
+ if (onChange) onChange(items);
105
+
106
+ // Update state
107
+ setItems(items);
108
+ };
109
+
95
110
  // Drag end handler
96
111
  const onDragEnd = (result: DropResult) => {
97
112
  // Dropped outside the list
@@ -109,13 +124,13 @@ export function DnDList<
109
124
  newItems.splice(result.destination.index, 0, removed);
110
125
 
111
126
  // Update the state
112
- setItems(newItems);
127
+ changeItems(newItems);
113
128
  };
114
129
 
115
130
  // Add item
116
131
  const addItem = (newItem: D) => {
117
132
  // Existence check
118
- if (items.some((item) => item[labelField] == newItem[labelField])) {
133
+ if (items.some((item) => item[labelField] === newItem[labelField])) {
119
134
  return false;
120
135
  }
121
136
 
@@ -123,7 +138,26 @@ export function DnDList<
123
138
  const newItems = [newItem, ...items];
124
139
 
125
140
  // Update the state
126
- setItems(newItems);
141
+ changeItems(newItems);
142
+
143
+ return true;
144
+ };
145
+
146
+ // Edit item
147
+ const editItem = (newItem: D, index: number) => {
148
+ // Existence check
149
+ if (items.some((item) => item[labelField] === newItem[labelField])) {
150
+ return false;
151
+ }
152
+
153
+ // Clone
154
+ const newItems = [...items];
155
+
156
+ // Remove the item
157
+ newItems.splice(index, 1, newItem);
158
+
159
+ // Update the state
160
+ changeItems(newItems);
127
161
 
128
162
  return true;
129
163
  };
@@ -146,7 +180,7 @@ export function DnDList<
146
180
  });
147
181
 
148
182
  // Update the state
149
- setItems(newItems);
183
+ changeItems(newItems);
150
184
 
151
185
  return newItems.length - items.length;
152
186
  };
@@ -160,10 +194,11 @@ export function DnDList<
160
194
  newItems.splice(index, 1);
161
195
 
162
196
  // Update the state
163
- setItems(newItems);
197
+ changeItems(newItems);
164
198
  };
165
199
 
166
200
  React.useEffect(() => {
201
+ // Load data
167
202
  loadData(name).then((items) => setItems(items));
168
203
  }, [name]);
169
204
 
@@ -212,7 +247,8 @@ export function DnDList<
212
247
  {children(
213
248
  item,
214
249
  index,
215
- deleteItem
250
+ deleteItem,
251
+ editItem
216
252
  )}
217
253
  </div>
218
254
  )}
@@ -25,7 +25,8 @@ import {
25
25
  Snackbar,
26
26
  styled,
27
27
  Switch,
28
- TextField
28
+ TextField,
29
+ Typography
29
30
  } from '@mui/material';
30
31
  import { Error, Info, Help, Warning, Done } from '@mui/icons-material';
31
32
  import React from 'react';
@@ -238,18 +239,30 @@ export class NotificationMU extends NotificationReact {
238
239
  ...rest
239
240
  } = this.inputProps ?? {};
240
241
 
242
+ const inputRef = React.createRef<HTMLInputElement>();
243
+ const errorRef = React.createRef<HTMLSpanElement>();
244
+
245
+ const setError = (error?: string) => {
246
+ if (errorRef.current == null) return;
247
+ errorRef.current.innerText = error ?? '';
248
+ };
249
+
241
250
  const handleSubmit = async (
242
251
  event: React.MouseEvent<HTMLButtonElement>
243
252
  ) => {
244
253
  // Result
245
- let result: boolean | void | PromiseLike<boolean | void> =
246
- undefined;
254
+ let result:
255
+ | boolean
256
+ | string
257
+ | void
258
+ | PromiseLike<boolean | string | void> = undefined;
259
+
260
+ const input = inputRef.current;
247
261
 
248
262
  if (this.onReturn) {
249
263
  // Inputs case, no HTMLForm set to value, set the current form
250
264
  if (inputs && value == null) value = event.currentTarget.form;
251
265
 
252
- const input = inputRef.current;
253
266
  if (input) {
254
267
  if (type === 'date') {
255
268
  const dateValue = input.valueAsDate;
@@ -284,13 +297,20 @@ export class NotificationMU extends NotificationReact {
284
297
  // Get the value
285
298
  // returns false to prevent default dismiss
286
299
  const v = await result;
287
- if (v === false) return;
300
+ if (v === false) {
301
+ input?.focus();
302
+ return;
303
+ }
304
+ if (typeof v === 'string') {
305
+ setError(v);
306
+ input?.focus();
307
+ return;
308
+ }
288
309
 
289
310
  this.dismiss();
290
311
  };
291
312
 
292
313
  let localInputs: React.ReactNode;
293
- let inputRef = React.createRef<HTMLInputElement>();
294
314
  let value: any = undefined;
295
315
 
296
316
  if (inputs == null) {
@@ -310,6 +330,7 @@ export class NotificationMU extends NotificationReact {
310
330
  localInputs = (
311
331
  <TextField
312
332
  inputRef={inputRef}
333
+ onChange={() => setError(undefined)}
313
334
  autoFocus
314
335
  margin="dense"
315
336
  fullWidth
@@ -341,6 +362,12 @@ export class NotificationMU extends NotificationReact {
341
362
  <DialogContent>
342
363
  <DialogContentText>{this.content}</DialogContentText>
343
364
  {localInputs}
365
+ <Typography
366
+ variant="caption"
367
+ display="block"
368
+ ref={errorRef}
369
+ color={(theme) => theme.palette.error.main}
370
+ />
344
371
  </DialogContent>
345
372
  <DialogActions>
346
373
  <Button
@@ -6,7 +6,7 @@ import { Labels } from '../../app/Labels';
6
6
  import { MUGlobal } from '../MUGlobal';
7
7
  import { CommonPage, CommonPageScrollContainer } from './CommonPage';
8
8
  import { CommonPageProps } from './CommonPageProps';
9
- import EditIcon from '@mui/icons-material/Edit';
9
+ import SaveIcon from '@mui/icons-material/Save';
10
10
 
11
11
  /**
12
12
  * Edit page router props, include 'id' (/:id) query parameter
@@ -70,7 +70,7 @@ export function EditPage(props: EditPageProps) {
70
70
  variant="contained"
71
71
  type="submit"
72
72
  fullWidth
73
- startIcon={<EditIcon />}
73
+ startIcon={<SaveIcon />}
74
74
  >
75
75
  {labels.save}
76
76
  </Button>