@axinom/mosaic-ui 0.35.0-rc.1 → 0.35.0-rc.10

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.
Files changed (73) hide show
  1. package/dist/components/DynamicDataList/DynamicDataList.d.ts +5 -14
  2. package/dist/components/DynamicDataList/DynamicDataList.d.ts.map +1 -1
  3. package/dist/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.d.ts +3 -1
  4. package/dist/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.d.ts.map +1 -1
  5. package/dist/components/DynamicDataList/DynamicListHeader/DynamicListHeader.d.ts +5 -1
  6. package/dist/components/DynamicDataList/DynamicListHeader/DynamicListHeader.d.ts.map +1 -1
  7. package/dist/components/DynamicDataList/DynamicListRow/DynamicListRow.d.ts +9 -10
  8. package/dist/components/DynamicDataList/DynamicListRow/DynamicListRow.d.ts.map +1 -1
  9. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.actions.d.ts +5 -0
  10. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.actions.d.ts.map +1 -0
  11. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.d.ts +4 -0
  12. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.d.ts.map +1 -0
  13. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.init.d.ts +4 -0
  14. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.init.d.ts.map +1 -0
  15. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.types.d.ts +38 -0
  16. package/dist/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.types.d.ts.map +1 -0
  17. package/dist/components/DynamicDataList/helpers/DynamicListReducer/index.d.ts +4 -0
  18. package/dist/components/DynamicDataList/helpers/DynamicListReducer/index.d.ts.map +1 -0
  19. package/dist/components/DynamicDataList/helpers/generateId.d.ts +6 -0
  20. package/dist/components/DynamicDataList/helpers/generateId.d.ts.map +1 -0
  21. package/dist/components/DynamicDataList/helpers/useColumnDefs.d.ts +14 -0
  22. package/dist/components/DynamicDataList/helpers/useColumnDefs.d.ts.map +1 -0
  23. package/dist/components/DynamicDataList/helpers/useDataHandler.d.ts +9 -0
  24. package/dist/components/DynamicDataList/helpers/useDataHandler.d.ts.map +1 -0
  25. package/dist/components/DynamicDataList/helpers/useRowAnimation.d.ts +12 -0
  26. package/dist/components/DynamicDataList/helpers/useRowAnimation.d.ts.map +1 -0
  27. package/dist/components/DynamicDataList/index.d.ts +1 -1
  28. package/dist/components/DynamicDataList/index.d.ts.map +1 -1
  29. package/dist/components/List/ListRow/ListRow.d.ts.map +1 -1
  30. package/dist/components/List/useColumnsSize.d.ts +1 -0
  31. package/dist/components/List/useColumnsSize.d.ts.map +1 -1
  32. package/dist/index.es.js +11 -3
  33. package/dist/index.es.js.map +1 -1
  34. package/dist/index.js +11 -3
  35. package/dist/index.js.map +1 -1
  36. package/package.json +5 -3
  37. package/src/components/DynamicDataList/DynamicDataList.scss +0 -61
  38. package/src/components/DynamicDataList/DynamicDataList.spec.tsx +126 -393
  39. package/src/components/DynamicDataList/DynamicDataList.stories.tsx +0 -5
  40. package/src/components/DynamicDataList/DynamicDataList.tsx +133 -600
  41. package/src/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.scss +4 -2
  42. package/src/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.spec.tsx +17 -44
  43. package/src/components/DynamicDataList/DynamicListDataEntry/DynamicListDataEntry.tsx +15 -22
  44. package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.scss +15 -10
  45. package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.spec.tsx +4 -1
  46. package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.tsx +16 -14
  47. package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.scss +16 -24
  48. package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.spec.tsx +26 -253
  49. package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.tsx +45 -139
  50. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.actions.spec.ts +276 -0
  51. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.actions.ts +86 -0
  52. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.init.spec.ts +118 -0
  53. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.init.ts +40 -0
  54. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.spec.ts +89 -0
  55. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.ts +42 -0
  56. package/src/components/DynamicDataList/helpers/DynamicListReducer/DynamicListReducer.types.ts +46 -0
  57. package/src/components/DynamicDataList/helpers/DynamicListReducer/index.ts +3 -0
  58. package/src/components/DynamicDataList/helpers/generateId.ts +10 -0
  59. package/src/components/DynamicDataList/helpers/useColumnDefs.ts +56 -0
  60. package/src/components/DynamicDataList/helpers/useDataHandler.ts +77 -0
  61. package/src/components/DynamicDataList/helpers/useRowAnimation.tsx +38 -0
  62. package/src/components/DynamicDataList/index.ts +2 -2
  63. package/src/components/FormElements/BooleanView/BooleanViewField.scss +2 -2
  64. package/src/components/FormStation/FormStation.scss +4 -0
  65. package/src/components/FormStation/FormStation.tsx +2 -2
  66. package/src/components/List/List.tsx +1 -1
  67. package/src/components/List/ListRow/ListRow.tsx +56 -50
  68. package/src/components/List/ListRow/Renderers/BooleanDotRenderer/BooleanDotRenderer.scss +6 -9
  69. package/src/components/List/ListRow/Renderers/BooleanDotRenderer/BooleanDotRenderer.spec.tsx +2 -2
  70. package/src/components/List/ListRow/Renderers/BooleanDotRenderer/BooleanDotRenderer.tsx +1 -1
  71. package/src/components/List/useColumnsSize.ts +20 -3
  72. package/src/styles/variables.scss +0 -5
  73. package/src/components/DynamicDataList/DynamicDataList.reposition.spec.tsx +0 -816
@@ -1,5 +1,6 @@
1
1
  import clsx from 'clsx';
2
2
  import React, { PropsWithChildren, useEffect, useState } from 'react';
3
+ import { DraggableProvided } from 'react-beautiful-dnd';
3
4
  import { noop } from '../../../helpers/utils';
4
5
  import { Data } from '../../../types/data';
5
6
  import { getTooltipText } from '../../../utils/ToolTipHelpers';
@@ -7,10 +8,7 @@ import { ActionData } from '../../Actions';
7
8
  import { Button, ButtonContext } from '../../Buttons';
8
9
  import { IconName, Icons } from '../../Icons';
9
10
  import { InlineMenu } from '../../InlineMenu';
10
- import {
11
- DynamicListColumn,
12
- DynamicListElevationOptions,
13
- } from '../DynamicDataList.model';
11
+ import { DynamicListColumn } from '../DynamicDataList.model';
14
12
  import classes from './DynamicListRow.scss';
15
13
 
16
14
  // TODO: Add sizing for DragIcon and Input container. Similar to ActionButton sizing
@@ -34,8 +32,6 @@ export interface DynamicListRowProps<T extends Data> {
34
32
  verticalTextAlign?: 'start' | 'center' | 'end';
35
33
  /** If set to true, the remove action button will be rendered (default: undefined) */
36
34
  allowRemove?: boolean;
37
- /** If set to true, rows can be repositioned using the input field (default: undefined) */
38
- allowReordering?: boolean;
39
35
  /** Property name that is used to determine data position (default: undefined) */
40
36
  positionKey?: keyof T;
41
37
  /** If set to true, this component can be dragged (default: undefined) */
@@ -44,16 +40,10 @@ export interface DynamicListRowProps<T extends Data> {
44
40
  dragging?: boolean;
45
41
  /** Whether or not the DDL is disabled (default: false) */
46
42
  disabled?: boolean;
47
- /** Emits when row dragging event starts. Event and current position are supplied as parameters */
48
- onDragStart?: (e: React.DragEvent<HTMLDivElement>, position: number) => void;
49
- /** Emits when the row has dropped. Event and new position target are supplied as parameters */
50
- onDrop?: (
51
- e: React.DragEvent<HTMLDivElement>,
52
- newPosition: number,
53
- elevation: DynamicListElevationOptions,
54
- ) => void;
55
- /** Emits when row dragging event ends. Event and data as supplied as parameters */
56
- onDragEnd?: (e: React.DragEvent<HTMLDivElement>) => void;
43
+ /** If set to true, the position column will be shown (default: false) */
44
+ showPositionColumn?: boolean;
45
+ /** If set to true, the action column will be shown (default: false) */
46
+ showActionColumn?: boolean;
57
47
  /** Emits when the position input has changed. Row data and the new position is supplied */
58
48
  onPositionInputChanged?: (
59
49
  currenPosition: number,
@@ -65,7 +55,9 @@ export interface DynamicListRowProps<T extends Data> {
65
55
  className?: string;
66
56
  /** CSS Class name provider for each row. Allows row style to be determined by data */
67
57
  rowClassNameProvider?: (data: T) => string;
58
+ /** Provide inline actions which are available through '...' context menu */
68
59
  inlineMenuActions?: (data: T) => ActionData[];
60
+ provided?: DraggableProvided;
69
61
  }
70
62
 
71
63
  export const DynamicListRow = <T extends Data>({
@@ -78,26 +70,26 @@ export const DynamicListRow = <T extends Data>({
78
70
  horizontalTextAlign,
79
71
  verticalTextAlign,
80
72
  allowRemove,
81
- allowReordering,
82
73
  positionKey,
83
74
  allowDragging,
84
- dragging,
85
75
  disabled,
86
- onDragStart = noop,
87
- onDrop = noop,
88
- onDragEnd = noop,
76
+ provided,
89
77
  onPositionInputChanged = noop,
90
78
  onActionClicked = noop,
91
79
  className = '',
92
80
  rowClassNameProvider,
93
81
  inlineMenuActions,
82
+ dragging = false,
83
+ showPositionColumn = false,
84
+ showActionColumn = false,
94
85
  }: PropsWithChildren<DynamicListRowProps<T>>): JSX.Element => {
95
86
  const customStyles = {
96
- gridAutoRows: rowHeight,
87
+ gridAutoRows: `minmax(50px, ${rowHeight})`,
97
88
  gridTemplateColumns: columnSizes,
98
89
  gridColumnGap: columnGap,
99
90
  justifyItems: horizontalTextAlign,
100
91
  alignItems: verticalTextAlign,
92
+ ...provided?.draggableProps.style,
101
93
  } as React.CSSProperties;
102
94
 
103
95
  const inlineMenuData = inlineMenuActions?.(data);
@@ -110,14 +102,6 @@ export const DynamicListRow = <T extends Data>({
110
102
  setPosition(positionKey ? Number(data[positionKey]) : undefined);
111
103
  }, [data, positionKey]);
112
104
 
113
- /* Resources used
114
- https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
115
- https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect
116
- https://www.youtube.com/watch?v=jfYWwQrtzzY
117
- https://www.youtube.com/watch?v=-MfTv5VRM0A
118
- */
119
- const [draggedOver, setDraggedOver] = useState<DynamicListElevationOptions>();
120
-
121
105
  /** Emits current position and new position when user changes position via input */
122
106
  const onPositionInputChangedHandler = (value: number): void => {
123
107
  if (positionKey !== undefined && typeof value === 'number') {
@@ -133,120 +117,49 @@ export const DynamicListRow = <T extends Data>({
133
117
  }
134
118
  };
135
119
 
136
- // Dragging has started(only emits if this is the element being dragged)
137
- const onDragStartHandler = (e: React.DragEvent<HTMLDivElement>): void => {
138
- e.stopPropagation();
139
- e.dataTransfer.effectAllowed = 'move'; // enable moving effects(for visual feedback)
140
- onDragStart(e, Number(position));
141
- };
142
-
143
- // An element that is being dragged has entered the drop zone
144
- const onDragEnterHandler = (e: React.DragEvent<HTMLDivElement>): void => {
145
- e.preventDefault();
146
- e.stopPropagation();
147
- };
148
-
149
- // Dragged element is currently over the drop zone
150
- const onDragOverHandler = (e: React.DragEvent<HTMLDivElement>): void => {
151
- e.preventDefault();
152
- e.stopPropagation();
153
- e.dataTransfer.dropEffect = 'move'; // change cursor
154
-
155
- const elevation = getElevation(
156
- e.clientY,
157
- e.currentTarget.getBoundingClientRect(),
158
- );
159
-
160
- setDraggedOver(elevation);
161
- };
162
-
163
- // Element has dropped(only emits if this was the element being dragged)
164
- const onDropHandler = (e: React.DragEvent<HTMLDivElement>): void => {
165
- e.preventDefault();
166
- e.stopPropagation();
167
-
168
- const elevation = getElevation(
169
- e.clientY,
170
- e.currentTarget.getBoundingClientRect(),
171
- );
172
-
173
- onDrop(e, Number(position), elevation);
174
-
175
- setDraggedOver(undefined); // remove dragged over styling
176
- };
177
-
178
- // An element that is being dragged has left the drop zone
179
- const onDragLeaveHandler = (e: React.DragEvent<HTMLDivElement>): void => {
180
- e.preventDefault();
181
- e.stopPropagation();
182
- setDraggedOver(undefined); // remove dragged over styling
183
- };
184
-
185
- // Dragging has ended(only emits if this was the element being dragged)
186
- const onDragEndhandler = (e: React.DragEvent<HTMLDivElement>): void => {
187
- e.preventDefault();
188
- e.stopPropagation();
189
- onDragEnd(e);
190
- };
191
-
192
120
  return (
193
121
  <div
194
122
  className={clsx(
195
123
  classes.container,
196
- {
197
- [classes.draggedAbove]: draggedOver === 'above',
198
- [classes.draggedBelow]: draggedOver === 'below',
199
- [classes.dragging]: dragging,
200
- [classes.disabled]: disabled,
201
- },
124
+ { [classes.dragging]: dragging },
202
125
  'dynamic-list-row-container',
203
126
  className,
204
127
  rowClassNameProvider ? rowClassNameProvider(data) : '',
205
128
  )}
206
- style={customStyles}
207
- draggable={Boolean(!disabled && allowDragging && allowReordering)}
208
- onDragStart={onDragStartHandler}
209
- onDragEnter={onDragEnterHandler}
210
- onDragOver={onDragOverHandler}
211
- onDrop={onDropHandler}
212
- onDragLeave={onDragLeaveHandler}
213
- onDragEnd={onDragEndhandler}
214
129
  data-test-id="dynamic-list-row"
130
+ ref={provided?.innerRef}
131
+ {...provided?.draggableProps}
132
+ style={customStyles}
215
133
  >
216
- {allowReordering === true &&
217
- // only show position column if allowReordering is true and the positionKey exists
218
- positionKey !== undefined && (
219
- <div className={classes.position}>
220
- {allowDragging && (
221
- <div className={classes.draggable}>
222
- <Icons icon={IconName.Drag} className={classes.dragIcon} />
223
- </div>
224
- )}
134
+ {showPositionColumn && (
135
+ <div className={classes.position}>
136
+ {allowDragging && (
137
+ <div className={classes.draggable} {...provided?.dragHandleProps}>
138
+ <Icons icon={IconName.Drag} className={classes.dragIcon} />
139
+ </div>
140
+ )}
225
141
 
226
- <div className={classes.input}>
227
- <input
228
- type="text"
229
- style={{ height: rowHeight, width: rowHeight }}
230
- onChange={(event) => {
231
- // check for valid number
232
- if (Number.isNaN(Number(event.target.value))) {
233
- return;
234
- } else {
235
- setPosition(Number(event.target.value));
236
- }
237
- }}
238
- value={position}
239
- disabled={disabled}
240
- onKeyDown={(event) =>
241
- event.key === 'Enter' &&
242
- onPositionInputChangedHandler(
243
- Number(event.currentTarget.value),
244
- )
142
+ <div className={classes.input}>
143
+ <input
144
+ type="text"
145
+ onChange={(event) => {
146
+ // check for valid number
147
+ if (Number.isNaN(Number(event.target.value))) {
148
+ return;
149
+ } else {
150
+ setPosition(Number(event.target.value));
245
151
  }
246
- />
247
- </div>
152
+ }}
153
+ value={position}
154
+ disabled={disabled}
155
+ onKeyDown={(event) =>
156
+ event.key === 'Enter' &&
157
+ onPositionInputChangedHandler(Number(event.currentTarget.value))
158
+ }
159
+ />
248
160
  </div>
249
- )}
161
+ </div>
162
+ )}
250
163
  {columns.map((column: DynamicListColumn<T>) => {
251
164
  const columnData: React.ReactNode = renderData<T>(
252
165
  column,
@@ -271,7 +184,7 @@ export const DynamicListRow = <T extends Data>({
271
184
  </div>
272
185
  );
273
186
  })}
274
- {(inlineMenuData || allowRemove) && (
187
+ {showActionColumn && (
275
188
  <div className={classes.actionButtonContainer}>
276
189
  {inlineMenuData && !!inlineMenuData.length && (
277
190
  <InlineMenu
@@ -302,13 +215,6 @@ export const DynamicListRow = <T extends Data>({
302
215
  );
303
216
  };
304
217
 
305
- const getElevation = (
306
- clientY: number,
307
- box: DOMRect,
308
- ): DynamicListElevationOptions => {
309
- return clientY - box.top - box.height / 2 < 0 ? 'above' : 'below';
310
- };
311
-
312
218
  const renderData = function <T extends Data>(
313
219
  column: DynamicListColumn<T>,
314
220
  data: T,
@@ -0,0 +1,276 @@
1
+ import { addItem, removeItem } from './DynamicListReducer.actions';
2
+
3
+ interface Data {
4
+ position?: number;
5
+ id: number;
6
+ }
7
+
8
+ const dataSet: Data[] = [
9
+ { position: 1, id: 1 },
10
+ { position: 2, id: 2 },
11
+ { position: 5, id: 5 },
12
+ { position: 6, id: 6 },
13
+ { position: 7, id: 7 },
14
+ { position: 8, id: 8 },
15
+ { position: 20, id: 20 },
16
+ { position: 100, id: 100 },
17
+ ];
18
+
19
+ const addItemCases = [
20
+ {
21
+ title: 'item at the beginning',
22
+ item: { position: 1, id: 999 },
23
+ result: [
24
+ { position: 1, id: 999 },
25
+ { position: 2, id: 1 },
26
+ { position: 3, id: 2 },
27
+ { position: 5, id: 5 },
28
+ { position: 6, id: 6 },
29
+ { position: 7, id: 7 },
30
+ { position: 8, id: 8 },
31
+ { position: 20, id: 20 },
32
+ { position: 100, id: 100 },
33
+ ],
34
+ },
35
+ {
36
+ title: 'item before a sequence',
37
+ item: { position: 4, id: 999 },
38
+ result: [
39
+ { position: 4, id: 999 },
40
+ { position: 1, id: 1 },
41
+ { position: 2, id: 2 },
42
+ { position: 5, id: 5 },
43
+ { position: 6, id: 6 },
44
+ { position: 7, id: 7 },
45
+ { position: 8, id: 8 },
46
+ { position: 20, id: 20 },
47
+ { position: 100, id: 100 },
48
+ ],
49
+ },
50
+ {
51
+ title: 'item in the beginning of a sequence',
52
+ item: { position: 5, id: 999 },
53
+ result: [
54
+ { position: 5, id: 999 },
55
+ { position: 1, id: 1 },
56
+ { position: 2, id: 2 },
57
+ { position: 6, id: 5 },
58
+ { position: 7, id: 6 },
59
+ { position: 8, id: 7 },
60
+ { position: 9, id: 8 },
61
+ { position: 20, id: 20 },
62
+ { position: 100, id: 100 },
63
+ ],
64
+ },
65
+ {
66
+ title: 'item in the middle of a sequence',
67
+ item: { position: 6, id: 999 },
68
+ result: [
69
+ { position: 6, id: 999 },
70
+ { position: 1, id: 1 },
71
+ { position: 2, id: 2 },
72
+ { position: 5, id: 5 },
73
+ { position: 7, id: 6 },
74
+ { position: 8, id: 7 },
75
+ { position: 9, id: 8 },
76
+ { position: 20, id: 20 },
77
+ { position: 100, id: 100 },
78
+ ],
79
+ },
80
+ {
81
+ title: 'item before the end of a sequence',
82
+ item: { position: 8, id: 999 },
83
+ result: [
84
+ { position: 8, id: 999 },
85
+ { position: 1, id: 1 },
86
+ { position: 2, id: 2 },
87
+ { position: 5, id: 5 },
88
+ { position: 6, id: 6 },
89
+ { position: 7, id: 7 },
90
+ { position: 9, id: 8 },
91
+ { position: 20, id: 20 },
92
+ { position: 100, id: 100 },
93
+ ],
94
+ },
95
+ {
96
+ title: 'item at the end of a sequence',
97
+ item: { position: 9, id: 999 },
98
+ result: [
99
+ { position: 9, id: 999 },
100
+ { position: 1, id: 1 },
101
+ { position: 2, id: 2 },
102
+ { position: 5, id: 5 },
103
+ { position: 6, id: 6 },
104
+ { position: 7, id: 7 },
105
+ { position: 8, id: 8 },
106
+ { position: 20, id: 20 },
107
+ { position: 100, id: 100 },
108
+ ],
109
+ },
110
+ {
111
+ title: 'item without a sequence',
112
+ item: { position: 50, id: 999 },
113
+ result: [
114
+ { position: 50, id: 999 },
115
+ { position: 1, id: 1 },
116
+ { position: 2, id: 2 },
117
+ { position: 5, id: 5 },
118
+ { position: 6, id: 6 },
119
+ { position: 7, id: 7 },
120
+ { position: 8, id: 8 },
121
+ { position: 20, id: 20 },
122
+ { position: 100, id: 100 },
123
+ ],
124
+ },
125
+ {
126
+ title: 'item at the end',
127
+ item: { position: 150, id: 999 },
128
+ result: [
129
+ { position: 150, id: 999 },
130
+ { position: 1, id: 1 },
131
+ { position: 2, id: 2 },
132
+ { position: 5, id: 5 },
133
+ { position: 6, id: 6 },
134
+ { position: 7, id: 7 },
135
+ { position: 8, id: 8 },
136
+ { position: 20, id: 20 },
137
+ { position: 100, id: 100 },
138
+ ],
139
+ },
140
+ ];
141
+
142
+ const removeItemCases = [
143
+ {
144
+ title: 'item at the beginning',
145
+ item: dataSet[0],
146
+ result: [
147
+ { position: 1, id: 2 },
148
+ { position: 5, id: 5 },
149
+ { position: 6, id: 6 },
150
+ { position: 7, id: 7 },
151
+ { position: 8, id: 8 },
152
+ { position: 20, id: 20 },
153
+ { position: 100, id: 100 },
154
+ ],
155
+ },
156
+ {
157
+ title: 'item before a sequence',
158
+ item: dataSet[1],
159
+ result: [
160
+ { position: 1, id: 1 },
161
+ { position: 5, id: 5 },
162
+ { position: 6, id: 6 },
163
+ { position: 7, id: 7 },
164
+ { position: 8, id: 8 },
165
+ { position: 20, id: 20 },
166
+ { position: 100, id: 100 },
167
+ ],
168
+ },
169
+ {
170
+ title: 'item in the beginning of a sequence',
171
+ item: dataSet[2],
172
+ result: [
173
+ { position: 1, id: 1 },
174
+ { position: 2, id: 2 },
175
+ { position: 5, id: 6 },
176
+ { position: 6, id: 7 },
177
+ { position: 7, id: 8 },
178
+ { position: 20, id: 20 },
179
+ { position: 100, id: 100 },
180
+ ],
181
+ },
182
+ {
183
+ title: 'item in the middle of a sequence',
184
+ item: dataSet[3],
185
+ result: [
186
+ { position: 1, id: 1 },
187
+ { position: 2, id: 2 },
188
+ { position: 5, id: 5 },
189
+ { position: 6, id: 7 },
190
+ { position: 7, id: 8 },
191
+ { position: 20, id: 20 },
192
+ { position: 100, id: 100 },
193
+ ],
194
+ },
195
+ {
196
+ title: 'item before the end of a sequence',
197
+ item: dataSet[4],
198
+ result: [
199
+ { position: 1, id: 1 },
200
+ { position: 2, id: 2 },
201
+ { position: 5, id: 5 },
202
+ { position: 6, id: 6 },
203
+ { position: 7, id: 8 },
204
+ { position: 20, id: 20 },
205
+ { position: 100, id: 100 },
206
+ ],
207
+ },
208
+ {
209
+ title: 'item at the end of a sequence',
210
+ item: dataSet[5],
211
+ result: [
212
+ { position: 1, id: 1 },
213
+ { position: 2, id: 2 },
214
+ { position: 5, id: 5 },
215
+ { position: 6, id: 6 },
216
+ { position: 7, id: 7 },
217
+ { position: 20, id: 20 },
218
+ { position: 100, id: 100 },
219
+ ],
220
+ },
221
+ {
222
+ title: 'item without a sequence',
223
+ item: dataSet[6],
224
+ result: [
225
+ { position: 1, id: 1 },
226
+ { position: 2, id: 2 },
227
+ { position: 5, id: 5 },
228
+ { position: 6, id: 6 },
229
+ { position: 7, id: 7 },
230
+ { position: 8, id: 8 },
231
+ { position: 100, id: 100 },
232
+ ],
233
+ },
234
+ {
235
+ title: 'item at the end',
236
+ item: dataSet[7],
237
+ result: [
238
+ { position: 1, id: 1 },
239
+ { position: 2, id: 2 },
240
+ { position: 5, id: 5 },
241
+ { position: 6, id: 6 },
242
+ { position: 7, id: 7 },
243
+ { position: 8, id: 8 },
244
+ { position: 20, id: 20 },
245
+ ],
246
+ },
247
+ ];
248
+
249
+ describe('Dynamic List Reducer Actions', () => {
250
+ it('addItem adds item without position', () => {
251
+ expect(addItem({ id: 999 }, dataSet, undefined)).toStrictEqual([
252
+ ...dataSet,
253
+ { id: 999 },
254
+ ]);
255
+ });
256
+
257
+ it.each(addItemCases)('addItem adds $title', ({ item, result }) => {
258
+ expect(addItem(item, dataSet, 'position')).toStrictEqual(result);
259
+ });
260
+
261
+ it('removeItem removes item without position', () => {
262
+ expect(removeItem(dataSet[3], dataSet, undefined)).toStrictEqual([
263
+ { position: 1, id: 1 },
264
+ { position: 2, id: 2 },
265
+ { position: 5, id: 5 },
266
+ { position: 7, id: 7 },
267
+ { position: 8, id: 8 },
268
+ { position: 20, id: 20 },
269
+ { position: 100, id: 100 },
270
+ ]);
271
+ });
272
+
273
+ it.each(removeItemCases)('removeItem removes $title', ({ item, result }) => {
274
+ expect(removeItem(item, dataSet, 'position')).toStrictEqual(result);
275
+ });
276
+ });
@@ -0,0 +1,86 @@
1
+ import { Data } from '../../../../types';
2
+ import { DynamicListReducerState } from './DynamicListReducer.types';
3
+
4
+ export const addItem = <T extends Data>(
5
+ item: T,
6
+ items: DynamicListReducerState<T>['items'],
7
+ positionPropertyName: DynamicListReducerState<T>['positionPropertyName'],
8
+ ): T[] => {
9
+ let newValue: T[] = [];
10
+ if (positionPropertyName) {
11
+ // We can safely push this item anywhere in the array as long as the reducer re-arranges the entire array based on positions
12
+ newValue.push(item);
13
+
14
+ items.forEach((value) => {
15
+ if (value[positionPropertyName] < item[positionPropertyName]) {
16
+ // Items before the new item
17
+ return newValue.push(value);
18
+ } else if (value[positionPropertyName] === item[positionPropertyName]) {
19
+ // If an item already exists in the same position, we need to increment the position of the existing item
20
+ newValue.push({
21
+ ...value,
22
+ [positionPropertyName]: value[positionPropertyName] + 1,
23
+ });
24
+ } else if (value[positionPropertyName] > item[positionPropertyName]) {
25
+ // Increment the rest of the items
26
+ if (newValue[newValue.length - 1].position === value.position) {
27
+ newValue.push({
28
+ ...value,
29
+ [positionPropertyName]: value[positionPropertyName] + 1,
30
+ });
31
+ } else {
32
+ return newValue.push(value);
33
+ }
34
+ }
35
+ });
36
+ } else {
37
+ newValue = [...items, item];
38
+ }
39
+
40
+ return newValue;
41
+ };
42
+
43
+ export const removeItem = <T extends Data>(
44
+ item: T,
45
+ items: DynamicListReducerState<T>['items'],
46
+ positionPropertyName: DynamicListReducerState<T>['positionPropertyName'],
47
+ ): T[] => {
48
+ let newValue: T[] = [];
49
+ if (positionPropertyName) {
50
+ let hasReachedGap = false;
51
+
52
+ items.forEach((value) => {
53
+ if (value[positionPropertyName] < item[positionPropertyName]) {
54
+ // Items before the removed item
55
+ return newValue.push(value);
56
+ } else if (value[positionPropertyName] > item[positionPropertyName]) {
57
+ // Check if a gap was reached
58
+ hasReachedGap =
59
+ hasReachedGap || isGap(items[items.indexOf(value) - 1], value);
60
+
61
+ // Items after the removed item
62
+ if (!hasReachedGap) {
63
+ return newValue.push({
64
+ ...value,
65
+ [positionPropertyName]: value[positionPropertyName] - 1,
66
+ });
67
+ } else {
68
+ return newValue.push(value);
69
+ }
70
+ }
71
+ });
72
+ } else {
73
+ newValue = items.filter((x) => x !== item);
74
+ }
75
+
76
+ return newValue;
77
+ };
78
+
79
+ const isGap = <T extends Data>(previousItem: T, nextItem: T): boolean => {
80
+ const previousPosition = previousItem?.position;
81
+ const nextPosition = nextItem?.position;
82
+
83
+ return (
84
+ previousPosition && nextPosition && previousPosition + 1 < nextPosition
85
+ );
86
+ };