@etsoo/react 1.3.97 → 1.4.1

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, onDelete: (index: number) => void) => React.ReactNode;
9
+ children: (item: D, index: number, deleteItem: (index: number) => void, editItem: (newItem: D, indexOrLabel: number | string) => void) => React.ReactNode;
10
10
  /**
11
11
  * List container component
12
12
  * https://javascript.plainenglish.io/building-a-polymorphic-component-in-react-and-typescript-d9f236950af4
@@ -19,7 +19,7 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
19
19
  /**
20
20
  * Get list item style callback
21
21
  */
22
- getItemStyle?: (isDragging: boolean) => CSSProperties;
22
+ getItemStyle?: (isDragging: boolean, index: number) => CSSProperties;
23
23
  /**
24
24
  * Get list style callback
25
25
  */
@@ -35,7 +35,7 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
35
35
  /**
36
36
  * Top and bottom sides renderer
37
37
  */
38
- sideRenderer?: (top: boolean, onAdd: (item: D) => boolean) => React.ReactNode;
38
+ sideRenderer?: (top: boolean, addItem: (item: D) => boolean, addItems: (items: D[]) => number) => React.ReactNode;
39
39
  /**
40
40
  * Name for hidden form input
41
41
  */
package/lib/mu/DnDList.js CHANGED
@@ -27,8 +27,8 @@ export function DnDList(props) {
27
27
  // Update the state
28
28
  setItems(newItems);
29
29
  };
30
- // Add handler
31
- const onAdd = (newItem) => {
30
+ // Add item
31
+ const addItem = (newItem) => {
32
32
  // Existence check
33
33
  if (items.some((item) => item[labelField] == newItem[labelField])) {
34
34
  return false;
@@ -39,8 +39,39 @@ export function DnDList(props) {
39
39
  setItems(newItems);
40
40
  return true;
41
41
  };
42
- // Delete handler
43
- const onDelete = (index) => {
42
+ // Edit item
43
+ const editItem = (newItem, indexOrLabel) => {
44
+ const index = typeof indexOrLabel === 'number'
45
+ ? indexOrLabel
46
+ : items.findIndex((item) => DataTypes.convert(item[labelField], 'string') ==
47
+ indexOrLabel);
48
+ if (index === -1)
49
+ return;
50
+ // Clone
51
+ const newItems = [...items];
52
+ // Remove the item
53
+ newItems.splice(index, 1, newItem);
54
+ // Update the state
55
+ setItems(newItems);
56
+ };
57
+ // Add items
58
+ const addItems = (inputItems) => {
59
+ // Clone
60
+ const newItems = [...items];
61
+ // Insert items
62
+ inputItems.forEach((newItem) => {
63
+ // Existence check
64
+ if (newItems.some((item) => item[labelField] == newItem[labelField])) {
65
+ return;
66
+ }
67
+ newItems.push(newItem);
68
+ });
69
+ // Update the state
70
+ setItems(newItems);
71
+ return newItems.length - items.length;
72
+ };
73
+ // Delete item
74
+ const deleteItem = (index) => {
44
75
  // Clone
45
76
  const newItems = [...items];
46
77
  // Remove the item
@@ -53,7 +84,7 @@ export function DnDList(props) {
53
84
  }, [name]);
54
85
  // Layout
55
86
  return (React.createElement(React.Fragment, null,
56
- sideRenderer && sideRenderer(true, onAdd),
87
+ sideRenderer && sideRenderer(true, addItem, addItems),
57
88
  React.createElement(Component, { ...componentProps },
58
89
  React.createElement(DragDropContext, { onDragEnd: onDragEnd },
59
90
  React.createElement(Droppable, { droppableId: name }, (provided, snapshot) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef, style: getListStyle(snapshot.isDraggingOver) },
@@ -63,12 +94,12 @@ export function DnDList(props) {
63
94
  if (id == null)
64
95
  return;
65
96
  return (React.createElement(Draggable, { key: id, draggableId: id, index: index }, (provided, snapshot) => (React.createElement("div", { ref: provided.innerRef, ...provided.draggableProps, ...provided.dragHandleProps, style: {
66
- ...getItemStyle(snapshot.isDragging),
97
+ ...getItemStyle(snapshot.isDragging, index),
67
98
  ...provided
68
99
  .draggableProps
69
100
  .style
70
- } }, children(item, index, onDelete)))));
101
+ } }, children(item, index, deleteItem, editItem)))));
71
102
  }),
72
103
  provided.placeholder))))),
73
- sideRenderer && sideRenderer(false, onAdd)));
104
+ sideRenderer && sideRenderer(false, addItem, addItems)));
74
105
  }
@@ -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,21 @@ 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 (Array.isArray(v)) {
180
+ if (!v[0]) {
181
+ const error = v[1];
182
+ setError(error);
183
+ input === null || input === void 0 ? void 0 : input.focus();
184
+ return;
185
+ }
186
+ }
170
187
  this.dismiss();
171
188
  };
172
189
  let localInputs;
173
- let inputRef = React.createRef();
174
190
  let value = undefined;
175
191
  if (inputs == null) {
176
192
  if (type === 'switch') {
@@ -180,7 +196,7 @@ export class NotificationMU extends NotificationReact {
180
196
  localInputs = React.createElement(Slider, { onChange: (_e, v) => (value = v) });
181
197
  }
182
198
  else {
183
- localInputs = (React.createElement(TextField, { inputRef: inputRef, autoFocus: true, margin: "dense", fullWidth: true, type: type, required: true, ...rest }));
199
+ localInputs = (React.createElement(TextField, { inputRef: inputRef, onChange: () => setError(undefined), autoFocus: true, margin: "dense", fullWidth: true, type: type, required: true, ...rest }));
184
200
  }
185
201
  }
186
202
  else {
@@ -193,7 +209,8 @@ export class NotificationMU extends NotificationReact {
193
209
  React.createElement("span", { className: "dialogTitle" }, title)),
194
210
  React.createElement(DialogContent, null,
195
211
  React.createElement(DialogContentText, null, this.content),
196
- localInputs),
212
+ localInputs,
213
+ React.createElement(Typography, { variant: "caption", display: "block", ref: errorRef, color: (theme) => theme.palette.error.main })),
197
214
  React.createElement(DialogActions, null,
198
215
  React.createElement(Button, { color: "secondary", onClick: () => {
199
216
  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.97",
3
+ "version": "1.4.1",
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.12",
54
+ "@etsoo/notificationbase": "^1.0.96",
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
- onDelete: (index: number) => void
20
+ deleteItem: (index: number) => void,
21
+ editItem: (newItem: D, indexOrLabel: number | string) => void
21
22
  ) => React.ReactNode;
22
23
 
23
24
  /**
@@ -34,7 +35,7 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
34
35
  /**
35
36
  * Get list item style callback
36
37
  */
37
- getItemStyle?: (isDragging: boolean) => CSSProperties;
38
+ getItemStyle?: (isDragging: boolean, index: number) => CSSProperties;
38
39
 
39
40
  /**
40
41
  * Get list style callback
@@ -56,7 +57,8 @@ export interface DnDListProps<D extends {}, E extends React.ElementType> {
56
57
  */
57
58
  sideRenderer?: (
58
59
  top: boolean,
59
- onAdd: (item: D) => boolean
60
+ addItem: (item: D) => boolean,
61
+ addItems: (items: D[]) => number
60
62
  ) => React.ReactNode;
61
63
 
62
64
  /**
@@ -111,8 +113,8 @@ export function DnDList<
111
113
  setItems(newItems);
112
114
  };
113
115
 
114
- // Add handler
115
- const onAdd = (newItem: D) => {
116
+ // Add item
117
+ const addItem = (newItem: D) => {
116
118
  // Existence check
117
119
  if (items.some((item) => item[labelField] == newItem[labelField])) {
118
120
  return false;
@@ -127,8 +129,53 @@ export function DnDList<
127
129
  return true;
128
130
  };
129
131
 
130
- // Delete handler
131
- const onDelete = (index: number) => {
132
+ // Edit item
133
+ const editItem = (newItem: D, indexOrLabel: number | string) => {
134
+ const index =
135
+ typeof indexOrLabel === 'number'
136
+ ? indexOrLabel
137
+ : items.findIndex(
138
+ (item) =>
139
+ DataTypes.convert(item[labelField], 'string') ==
140
+ indexOrLabel
141
+ );
142
+ if (index === -1) return;
143
+
144
+ // Clone
145
+ const newItems = [...items];
146
+
147
+ // Remove the item
148
+ newItems.splice(index, 1, newItem);
149
+
150
+ // Update the state
151
+ setItems(newItems);
152
+ };
153
+
154
+ // Add items
155
+ const addItems = (inputItems: D[]) => {
156
+ // Clone
157
+ const newItems = [...items];
158
+
159
+ // Insert items
160
+ inputItems.forEach((newItem) => {
161
+ // Existence check
162
+ if (
163
+ newItems.some((item) => item[labelField] == newItem[labelField])
164
+ ) {
165
+ return;
166
+ }
167
+
168
+ newItems.push(newItem);
169
+ });
170
+
171
+ // Update the state
172
+ setItems(newItems);
173
+
174
+ return newItems.length - items.length;
175
+ };
176
+
177
+ // Delete item
178
+ const deleteItem = (index: number) => {
132
179
  // Clone
133
180
  const newItems = [...items];
134
181
 
@@ -146,7 +193,7 @@ export function DnDList<
146
193
  // Layout
147
194
  return (
148
195
  <React.Fragment>
149
- {sideRenderer && sideRenderer(true, onAdd)}
196
+ {sideRenderer && sideRenderer(true, addItem, addItems)}
150
197
  <Component {...componentProps}>
151
198
  <DragDropContext onDragEnd={onDragEnd}>
152
199
  <Droppable droppableId={name}>
@@ -177,7 +224,8 @@ export function DnDList<
177
224
  {...provided.dragHandleProps}
178
225
  style={{
179
226
  ...getItemStyle(
180
- snapshot.isDragging
227
+ snapshot.isDragging,
228
+ index
181
229
  ),
182
230
  ...provided
183
231
  .draggableProps
@@ -187,7 +235,8 @@ export function DnDList<
187
235
  {children(
188
236
  item,
189
237
  index,
190
- onDelete
238
+ deleteItem,
239
+ editItem
191
240
  )}
192
241
  </div>
193
242
  )}
@@ -200,7 +249,7 @@ export function DnDList<
200
249
  </Droppable>
201
250
  </DragDropContext>
202
251
  </Component>
203
- {sideRenderer && sideRenderer(false, onAdd)}
252
+ {sideRenderer && sideRenderer(false, addItem, addItems)}
204
253
  </React.Fragment>
205
254
  );
206
255
  }
@@ -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,31 @@ 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> =
254
+ let result:
255
+ | boolean
256
+ | [boolean, string | undefined]
257
+ | void
258
+ | PromiseLike<boolean | [boolean, string | undefined] | void> =
246
259
  undefined;
247
260
 
261
+ const input = inputRef.current;
262
+
248
263
  if (this.onReturn) {
249
264
  // Inputs case, no HTMLForm set to value, set the current form
250
265
  if (inputs && value == null) value = event.currentTarget.form;
251
266
 
252
- const input = inputRef.current;
253
267
  if (input) {
254
268
  if (type === 'date') {
255
269
  const dateValue = input.valueAsDate;
@@ -284,13 +298,23 @@ export class NotificationMU extends NotificationReact {
284
298
  // Get the value
285
299
  // returns false to prevent default dismiss
286
300
  const v = await result;
287
- if (v === false) return;
301
+ if (v === false) {
302
+ input?.focus();
303
+ return;
304
+ }
305
+ if (Array.isArray(v)) {
306
+ if (!v[0]) {
307
+ const error = v[1];
308
+ setError(error);
309
+ input?.focus();
310
+ return;
311
+ }
312
+ }
288
313
 
289
314
  this.dismiss();
290
315
  };
291
316
 
292
317
  let localInputs: React.ReactNode;
293
- let inputRef = React.createRef<HTMLInputElement>();
294
318
  let value: any = undefined;
295
319
 
296
320
  if (inputs == null) {
@@ -310,6 +334,7 @@ export class NotificationMU extends NotificationReact {
310
334
  localInputs = (
311
335
  <TextField
312
336
  inputRef={inputRef}
337
+ onChange={() => setError(undefined)}
313
338
  autoFocus
314
339
  margin="dense"
315
340
  fullWidth
@@ -341,6 +366,12 @@ export class NotificationMU extends NotificationReact {
341
366
  <DialogContent>
342
367
  <DialogContentText>{this.content}</DialogContentText>
343
368
  {localInputs}
369
+ <Typography
370
+ variant="caption"
371
+ display="block"
372
+ ref={errorRef}
373
+ color={(theme) => theme.palette.error.main}
374
+ />
344
375
  </DialogContent>
345
376
  <DialogActions>
346
377
  <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>