playbook_ui 12.26.0.pre.alpha.multiselectfixes797 → 12.26.0.pre.alpha.railsmultilevelimprovements805

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb28619d3962e184fa90f3ae74186c2ae1af998512701e96ac16b4773af46e03
4
- data.tar.gz: 1292e10c121acf557424b1182a40d975c3d048beb1671e04953845033778e049
3
+ metadata.gz: 79c732fa4dc676050bdbc6dda57f9a180670d812ffeeb73ba5d410a683a2c6bf
4
+ data.tar.gz: 332c5b9b341d69f47d83af65dfd5936918ee3b7e4149884892a8c4926b7ec3e7
5
5
  SHA512:
6
- metadata.gz: 5ca7d7d6b91997cd0cdf8f9f9c1064006b83080c618bb5412f291b21a9c3e973f42001b6fae6b4ddec7e37f19c1dac859c926ac8d4bd987f063ae3e0fb9a373d
7
- data.tar.gz: bc47e4d4e8b137a3ac76db1f2c3578295dad61e9dd50a9af8b1b91a27274e14d679749144ca36cc01405971cd3bf728624294b1dde468529abe568a821351eab
6
+ metadata.gz: bd51e6d5feffbe630c6e315e17e108d3f2c2d55a55b2ba12fbe14894950f61e9d457cb29899993a7f990564140e95e78c8251e675cfa7f3048dacfb7cb24d72f
7
+ data.tar.gz: fd53b4b633ea48c3b6db1f2f87c017509db18cf1d7732d9a1992ee3147e8df41d6e0b05fa1bf5f876c2304c3ae71ddb8627deb1e5abd65de8d8f14ccac9bc139
@@ -210,22 +210,3 @@ export const removeChildrenIfParentChecked = (
210
210
  );
211
211
  setDefaultReturn([...filteredDefaultArray, items]);
212
212
  };
213
-
214
- export const areAllCheckedFalse = (data:any) => {
215
- if (!Array.isArray(data)) {
216
- return;
217
- }
218
- for (const item of data) {
219
- if (item.checked !== false) {
220
- return false; // Return false if any item is not checked: false
221
- }
222
-
223
- if (item.children && item.children.length > 0) {
224
- if (!areAllCheckedFalse(item.children)) {
225
- return false; // Return false if any nested item is not checked: false
226
- }
227
- }
228
- }
229
-
230
- return true; // Return true if all items are checked: false
231
- };
@@ -1,11 +1,11 @@
1
- import React, { useState, useEffect, useRef } from "react";
2
- import classnames from "classnames";
3
- import { buildAriaProps, buildCss, buildDataProps } from "../utilities/props";
4
- import { globalProps, GlobalProps } from "../utilities/globalProps";
5
- import Icon from "../pb_icon/_icon";
6
- import Checkbox from "../pb_checkbox/_checkbox";
7
- import FormPill from "../pb_form_pill/_form_pill";
8
- import CircleIconButton from "../pb_circle_icon_button/_circle_icon_button";
1
+ import React, { useState, useEffect, useRef } from "react"
2
+ import classnames from "classnames"
3
+ import { buildAriaProps, buildCss, buildDataProps } from "../utilities/props"
4
+ import { globalProps, GlobalProps } from "../utilities/globalProps"
5
+ import Icon from "../pb_icon/_icon"
6
+ import Checkbox from "../pb_checkbox/_checkbox"
7
+ import FormPill from "../pb_form_pill/_form_pill"
8
+ import CircleIconButton from "../pb_circle_icon_button/_circle_icon_button"
9
9
  import {
10
10
  unCheckIt,
11
11
  getAncestorsOfUnchecked,
@@ -18,18 +18,19 @@ import {
18
18
  recursiveReturnOnlyParent,
19
19
  removeChildrenIfParentChecked,
20
20
  getChildIds,
21
- areAllCheckedFalse
22
- } from "./_helper_functions";
21
+ } from "./_helper_functions"
23
22
 
24
23
  type MultiLevelSelectProps = {
25
- aria?: { [key: string]: string };
26
- className?: string;
27
- data?: { [key: string]: string };
28
- id?: string;
29
- returnAllSelected?: boolean;
30
- treeData?: { [key: string]: string }[];
31
- onSelect?: (prop: { [key: string]: any }) => void;
32
- } & GlobalProps;
24
+ aria?: { [key: string]: string }
25
+ className?: string
26
+ data?: { [key: string]: string }
27
+ id?: string
28
+ name?: string
29
+ returnCompleteData?: boolean
30
+ returnAllSelected?: boolean
31
+ treeData?: { [key: string]: string }[]
32
+ onSelect?: (prop: { [key: string]: any }) => void
33
+ } & GlobalProps
33
34
 
34
35
  const MultiLevelSelect = (props: MultiLevelSelectProps) => {
35
36
  const {
@@ -37,212 +38,228 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
37
38
  className,
38
39
  data = {},
39
40
  id,
41
+ name,
40
42
  returnAllSelected = false,
43
+ returnCompleteData = false,
41
44
  treeData,
42
45
  onSelect = () => {},
43
- } = props;
46
+ } = props
44
47
 
45
- const ariaProps = buildAriaProps(aria);
46
- const dataProps = buildDataProps(data);
48
+ const ariaProps = buildAriaProps(aria)
49
+ const dataProps = buildDataProps(data)
47
50
  const classes = classnames(
48
51
  buildCss("pb_multi_level_select"),
49
52
  globalProps(props),
50
53
  className
51
- );
54
+ )
52
55
 
53
- const dropdownRef = useRef(null);
56
+ const dropdownRef = useRef(null)
54
57
 
55
58
  //state for whether dropdown is open or closed
56
- const [isClosed, setIsClosed] = useState(true);
59
+ const [isClosed, setIsClosed] = useState(true)
57
60
  //state from onchange for textinput, to use for filtering to create typeahead
58
- const [filterItem, setFilterItem] = useState("");
61
+ const [filterItem, setFilterItem] = useState("")
59
62
  //this is essentially the return that the user will get when they use the kit
60
- const [returnedArray, setReturnedArray] = useState([]);
63
+ const [returnedArray, setReturnedArray] = useState([])
61
64
  //formattedData with checked and parent_id added
62
- const [formattedData, setFormattedData] = useState(treeData);
65
+ const [formattedData, setFormattedData] = useState(treeData)
63
66
  //toggle chevron in dropdown
64
67
  //@ts-ignore
65
- const [isToggled, setIsToggled] = useState<{ [id: number]: boolean }>({});
68
+ const [isToggled, setIsToggled] = useState<{ [id: number]: boolean }>({})
66
69
  //state for return for default
67
- const [defaultReturn, setDefaultReturn] = useState([]);
70
+ const [defaultReturn, setDefaultReturn] = useState([])
68
71
 
69
- useEffect(() => {
70
- let el = document.getElementById(`pb_data_wrapper_${id}`);
71
- if (el) {
72
- el.setAttribute(
73
- "data-tree",
74
- JSON.stringify(returnAllSelected ? returnedArray : defaultReturn)
75
- );
72
+ const [formattedReturn, setFormattedReturn] = useState([])
73
+
74
+ const getSelectedIds = (selectedData: { [key: string]: any }[]) => {
75
+ return selectedData
76
+ .map((item) => item.id)
77
+ .filter((value, index, self) => self.indexOf(value) === index)
78
+ }
79
+
80
+ const getValues = () => {
81
+ if(returnCompleteData){
82
+ if(returnAllSelected){
83
+ return setFormattedReturn(returnedArray)
84
+ } else {
85
+ return setFormattedReturn(defaultReturn)
76
86
  }
77
- returnAllSelected
78
- ? onSelect(returnedArray)
79
- : onSelect(
80
- defaultReturn.filter(
81
- (item, index, self) =>
82
- index === self.findIndex((obj) => obj.id === item.id)
83
- )
84
- );
85
- console.log("RETURN", returnedArray)
86
- }, [returnedArray, defaultReturn]);
87
+ } else {
88
+ if(returnAllSelected){
89
+ setFormattedReturn(getSelectedIds(returnedArray))
90
+ } else {
91
+ setFormattedReturn(getSelectedIds(defaultReturn))
92
+ }
93
+ }
94
+ }
95
+
96
+ const updateHiddenInputValue = (value: any) => {
97
+ const hiddenInput = document.querySelector(
98
+ "input#" + id
99
+ ) as HTMLInputElement
100
+ if (hiddenInput) {
101
+ hiddenInput.value = JSON.stringify(value)
102
+ }
103
+ }
87
104
 
88
105
  useEffect(() => {
89
- console.log("TREEDATA", treeData)
90
- //Create new formattedData array for use
91
- setFormattedData(addCheckedAndParentProperty(treeData));
92
- //if any items already checked in first render, set return accordingly
93
- const initialChecked = getCheckedItems(treeData)
94
- console.log("INITIAl CHECKED", initialChecked)
95
- const initialUnchecked = areAllCheckedFalse(treeData)
96
- console.log("INIITAL UNCHECKED", initialUnchecked)
97
- initialChecked && returnAllSelected && setReturnedArray(initialChecked)
98
- initialChecked && !returnAllSelected && setDefaultReturn(initialChecked)
99
- // initialUnchecked && returnAllSelected && setReturnedArray([])
100
- // initialUnchecked && !returnAllSelected && setDefaultReturn([])
106
+ getValues()
107
+ }, [returnedArray, defaultReturn])
101
108
 
102
- }, [treeData]);
109
+ useEffect(() => {
110
+ onSelect(formattedReturn)
111
+ updateHiddenInputValue(formattedReturn)
112
+ }, [formattedReturn])
103
113
 
104
- useEffect(()=> {
114
+ useEffect(() => {
115
+ //Create new formattedData array for use
116
+ setFormattedData(addCheckedAndParentProperty(treeData))
105
117
  // Function to handle clicks outside the dropdown
106
118
  const handleClickOutside = (event: any) => {
107
119
  if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
108
- setIsClosed(true);
120
+ setIsClosed(true)
109
121
  }
110
- };
111
- // Attach the event listener
112
- window.addEventListener("click", handleClickOutside);
113
- // Clean up the event listener on unmount
114
- return () => {
115
- window.removeEventListener("click", handleClickOutside);
116
- };
117
- },[])
122
+ }
123
+ //if any items already checked in first render, set return accordingly
124
+ const initialChecked = getCheckedItems(treeData)
125
+ initialChecked && returnAllSelected && setReturnedArray(initialChecked)
126
+ initialChecked && !returnAllSelected && setDefaultReturn(initialChecked)
127
+
128
+ // Attach the event listener
129
+ window.addEventListener("click", handleClickOutside)
130
+ // Clean up the event listener on unmount
131
+ return () => {
132
+ window.removeEventListener("click", handleClickOutside)
133
+ }
134
+ }, [])
118
135
 
119
136
  //function to map over data and add parent_id + depth property to each item
120
137
  const addCheckedAndParentProperty = (
121
138
  treeData: { [key: string]: any }[],
122
139
  parent_id: string = null,
123
- depth: number = 0,
140
+ depth: number = 0
124
141
  ) => {
125
142
  if (!Array.isArray(treeData)) {
126
- return;
143
+ return
127
144
  }
128
145
  return treeData.map((item: { [key: string]: any } | any) => {
129
146
  const newItem = {
130
147
  ...item,
131
148
  parent_id,
132
149
  depth,
133
- };
150
+ }
134
151
  if (newItem.children && newItem.children.length > 0) {
135
152
  newItem.children = addCheckedAndParentProperty(
136
153
  newItem.children,
137
154
  newItem.id,
138
- depth + 1,
139
- );
155
+ depth + 1
156
+ )
140
157
  }
141
- return newItem;
142
- });
143
- };
158
+ return newItem
159
+ })
160
+ }
144
161
 
145
162
  //click event for x on form pill
146
163
  const handlePillClose = (event: any, clickedItem: { [key: string]: any }) => {
147
164
  // prevents the dropdown from closing when clicking on the pill
148
- event.stopPropagation();
165
+ event.stopPropagation()
149
166
  //logic for removing items from returnArray or defaultReturn when pills clicked
150
167
  if (returnAllSelected) {
151
168
  if (returnedArray.includes(clickedItem)) {
152
169
  if (clickedItem.children && clickedItem.children.length > 0) {
153
- const childrenOfChecked = getChildIds(clickedItem, returnedArray);
170
+ const childrenOfChecked = getChildIds(clickedItem, returnedArray)
154
171
  const updatedFiltered = returnedArray
155
172
  .filter((item) => item !== clickedItem)
156
- .filter((item) => !childrenOfChecked.includes(item.id));
157
- setReturnedArray(updatedFiltered);
173
+ .filter((item) => !childrenOfChecked.includes(item.id))
174
+ setReturnedArray(updatedFiltered)
158
175
  } else {
159
176
  const updatedFiltered = returnedArray.filter(
160
177
  (item) => item !== clickedItem
161
- );
162
- setReturnedArray(updatedFiltered);
178
+ )
179
+ setReturnedArray(updatedFiltered)
163
180
  }
164
181
  }
165
182
  } else {
166
183
  if (defaultReturn.includes(clickedItem)) {
167
- getAncestorsOfUnchecked(formattedData, clickedItem);
168
- const newChecked = getCheckedItems(formattedData);
184
+ getAncestorsOfUnchecked(formattedData, clickedItem)
185
+ const newChecked = getCheckedItems(formattedData)
169
186
  const filteredReturn = updateReturnItems(newChecked).filter(
170
187
  (item) => item.id !== clickedItem.id
171
- );
172
- setDefaultReturn(filteredReturn);
188
+ )
189
+ setDefaultReturn(filteredReturn)
173
190
  }
174
191
  }
175
192
  if (clickedItem.children && clickedItem.children.length > 0) {
176
- unCheckedRecursive(clickedItem);
193
+ unCheckedRecursive(clickedItem)
177
194
  }
178
195
  //logic to uncheck clickedItem in formattedData
179
- unCheckIt(formattedData, clickedItem.id);
180
- };
196
+ unCheckIt(formattedData, clickedItem.id)
197
+ }
181
198
 
182
199
  //handle click on input wrapper(entire div with pills, typeahead, etc) so it doesn't close when input or form pill is clicked
183
200
  const handleInputWrapperClick = (e: any) => {
184
- e.stopPropagation();
201
+ e.stopPropagation()
185
202
  if (
186
203
  e.target.id === "multiselect_input" ||
187
204
  e.target.classList.contains("pb_form_pill_tag")
188
205
  ) {
189
- return;
206
+ return
190
207
  }
191
- setIsClosed(!isClosed);
192
- };
208
+ setIsClosed(!isClosed)
209
+ }
193
210
 
194
211
  //Main function to handle any click inside dropdown
195
212
  const handledropdownItemClick = (e: any) => {
196
- const clickedItem = e.target.parentNode.id;
213
+ const clickedItem = e.target.parentNode.id
197
214
  //setting filterItem to "" will clear textinput and clear typeahead
198
- setFilterItem("");
215
+ setFilterItem("")
199
216
 
200
- const filtered = filterFormattedDataById(formattedData, clickedItem);
217
+ const filtered = filterFormattedDataById(formattedData, clickedItem)
201
218
  //check and uncheck all children of checked/unchecked parent item
202
219
  if (filtered[0].children && filtered[0].children.length > 0) {
203
220
  if (filtered[0].checked) {
204
221
  filtered[0].children.forEach((item: { [key: string]: any }) => {
205
- checkedRecursive(item);
206
- });
222
+ checkedRecursive(item)
223
+ })
207
224
  } else if (!filtered[0].checked) {
208
225
  filtered[0].children.forEach((item: { [key: string]: any }) => {
209
- unCheckedRecursive(item);
210
- });
226
+ unCheckedRecursive(item)
227
+ })
211
228
  }
212
229
  }
213
230
 
214
- const checkedItems = getCheckedItems(formattedData);
231
+ const checkedItems = getCheckedItems(formattedData)
215
232
 
216
233
  //checking and unchecking items for returnAllSelected variant
217
234
  if (returnedArray.includes(filtered[0])) {
218
235
  if (!filtered[0].checked) {
219
236
  if (filtered[0].children && filtered[0].children.length > 0) {
220
- const childrenOfChecked = getChildIds(filtered[0], returnedArray);
237
+ const childrenOfChecked = getChildIds(filtered[0], returnedArray)
221
238
  const updatedFiltered = returnedArray
222
239
  .filter((item) => item !== filtered[0])
223
- .filter((item) => !childrenOfChecked.includes(item.id));
240
+ .filter((item) => !childrenOfChecked.includes(item.id))
224
241
 
225
- setReturnedArray(updatedFiltered);
242
+ setReturnedArray(updatedFiltered)
226
243
  } else {
227
244
  const updatedFiltered = returnedArray.filter(
228
245
  (item) => item !== filtered[0]
229
- );
230
- setReturnedArray(updatedFiltered);
246
+ )
247
+ setReturnedArray(updatedFiltered)
231
248
  }
232
249
  }
233
250
  } else {
234
- setReturnedArray(checkedItems);
251
+ setReturnedArray(checkedItems)
235
252
  }
236
253
 
237
254
  //when item is unchecked for default variant
238
255
  if (!filtered[0].checked && !returnAllSelected) {
239
256
  //uncheck parent and grandparent if any child unchecked
240
- getAncestorsOfUnchecked(formattedData, filtered[0]);
257
+ getAncestorsOfUnchecked(formattedData, filtered[0])
241
258
 
242
- const newChecked = getCheckedItems(formattedData);
259
+ const newChecked = getCheckedItems(formattedData)
243
260
  //get all checked items, and filter to check if all children checked, if yes return only parent
244
- const filteredReturn = updateReturnItems(newChecked);
245
- setDefaultReturn(filteredReturn);
261
+ const filteredReturn = updateReturnItems(newChecked)
262
+ setDefaultReturn(filteredReturn)
246
263
  }
247
264
 
248
265
  //when item is checked for default variant
@@ -253,7 +270,7 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
253
270
  filtered[0],
254
271
  defaultReturn,
255
272
  setDefaultReturn
256
- );
273
+ )
257
274
  }
258
275
 
259
276
  //if clicked item has parent_id, find parent and check if all children checked or not
@@ -263,26 +280,26 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
263
280
  formattedData,
264
281
  defaultReturn,
265
282
  setDefaultReturn
266
- );
283
+ )
267
284
  } else {
268
- setDefaultReturn([filtered[0]]);
285
+ setDefaultReturn([filtered[0]])
269
286
  }
270
287
  }
271
- };
288
+ }
272
289
 
273
290
  //handle click on chevron toggles in dropdown
274
291
  const handleToggleClick = (id: string, event: React.MouseEvent) => {
275
- event.stopPropagation();
292
+ event.stopPropagation()
276
293
  setIsToggled((prevState: { [id: string]: boolean }) => ({
277
294
  ...prevState,
278
295
  [id]: !prevState[id],
279
- }));
280
- const clickedItem = filterFormattedDataById(formattedData, id);
296
+ }))
297
+ const clickedItem = filterFormattedDataById(formattedData, id)
281
298
 
282
299
  if (clickedItem) {
283
- clickedItem[0].expanded = !clickedItem[0].expanded;
300
+ clickedItem[0].expanded = !clickedItem[0].expanded
284
301
  }
285
- };
302
+ }
286
303
 
287
304
  //rendering formattedData to UI based on typeahead
288
305
  const renderNestedOptions = (items: { [key: string]: any }[]) => {
@@ -292,35 +309,29 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
292
309
  items.map((item: { [key: string]: any }) => {
293
310
  return (
294
311
  <>
295
- <li
296
- key={item.id}
297
- className="dropdown_item"
298
- data-name={item.id}
299
- >
300
- <div className="dropdown_item_checkbox_row">
301
- <div
302
- key={
303
- item.expanded ? "chevron-down" : "chevron-right"
304
- }
305
- >
312
+ <li key={item.id} className='dropdown_item' data-name={item.id}>
313
+ <div className='dropdown_item_checkbox_row'>
314
+ <div key={item.expanded ? "chevron-down" : "chevron-right"}>
306
315
  <CircleIconButton
307
- icon={
308
- item.expanded ? "chevron-down" : "chevron-right"
316
+ icon={item.expanded ? "chevron-down" : "chevron-right"}
317
+ className={
318
+ item.children && item.children.length > 0
319
+ ? ""
320
+ : "toggle_icon"
309
321
  }
310
- className={item.children && item.children.length > 0 ? "" : "toggle_icon"}
311
322
  onClick={(event) => handleToggleClick(item.id, event)}
312
- variant="link"
323
+ variant='link'
313
324
  />
314
325
  </div>
315
326
  <Checkbox text={item.label} id={item.id}>
316
327
  <input
317
328
  checked={item.checked}
318
- type="checkbox"
329
+ type='checkbox'
319
330
  name={item.label}
320
331
  value={item.label}
321
332
  onChange={(e) => {
322
- item.checked = !item.checked;
323
- handledropdownItemClick(e);
333
+ item.checked = !item.checked
334
+ handledropdownItemClick(e)
324
335
  }}
325
336
  />
326
337
  </Checkbox>
@@ -333,23 +344,29 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
333
344
  )}
334
345
  </li>
335
346
  </>
336
- );
347
+ )
337
348
  })}
338
349
  </ul>
339
- );
340
- };
350
+ )
351
+ }
341
352
 
342
353
  return (
343
354
  <div {...ariaProps} {...dataProps} className={classes} id={id}>
344
- <div ref={dropdownRef} className="wrapper">
345
- <div className="input_wrapper" onClick={handleInputWrapperClick}>
346
- <div className="input_inner_container">
355
+ <div ref={dropdownRef} className='wrapper'>
356
+ <div className='input_wrapper' onClick={handleInputWrapperClick}>
357
+ <div className='input_inner_container'>
358
+ <input
359
+ type='hidden'
360
+ id={id}
361
+ name={name}
362
+ value={JSON.stringify(formattedReturn)}
363
+ />
347
364
  {returnedArray.length !== 0 && returnAllSelected
348
365
  ? returnedArray.map((item, index) => (
349
366
  <FormPill
350
367
  key={index}
351
368
  text={item.label}
352
- size="small"
369
+ size='small'
353
370
  onClick={(event) => handlePillClose(event, item)}
354
371
  />
355
372
  ))
@@ -365,29 +382,29 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
365
382
  <FormPill
366
383
  key={index}
367
384
  text={item.label}
368
- size="small"
385
+ size='small'
369
386
  onClick={(event) => handlePillClose(event, item)}
370
387
  />
371
388
  ))}
372
389
  {returnedArray.length !== 0 && returnAllSelected && <br />}
373
390
  {defaultReturn.length !== 0 && !returnAllSelected && <br />}
374
391
  <input
375
- id="multiselect_input"
392
+ id='multiselect_input'
376
393
  onChange={(e) => {
377
- setFilterItem(e.target.value);
394
+ setFilterItem(e.target.value)
378
395
  }}
379
- placeholder="Start typing..."
396
+ placeholder='Start typing...'
380
397
  value={filterItem}
381
398
  onClick={() => setIsClosed(false)}
382
399
  />
383
400
  </div>
384
401
  {isClosed ? (
385
- <div key="chevron-down">
386
- <Icon icon="chevron-down" />
402
+ <div key='chevron-down'>
403
+ <Icon icon='chevron-down' />
387
404
  </div>
388
405
  ) : (
389
- <div key="chevron-up">
390
- <Icon icon="chevron-up" />
406
+ <div key='chevron-up'>
407
+ <Icon icon='chevron-up' />
391
408
  </div>
392
409
  )}
393
410
  </div>
@@ -398,7 +415,7 @@ const MultiLevelSelect = (props: MultiLevelSelectProps) => {
398
415
  </div>
399
416
  </div>
400
417
  </div>
401
- );
402
- };
418
+ )
419
+ }
403
420
 
404
- export default MultiLevelSelect;
421
+ export default MultiLevelSelect
@@ -61,12 +61,12 @@
61
61
  ],
62
62
  },
63
63
  ],
64
- }] %>
64
+ }] %>
65
65
 
66
66
 
67
67
  <%= pb_rails("multi_level_select", props: {
68
68
  id: "default-multi-level-select",
69
69
  tree_data:treeData
70
- }) %>
70
+ }) %>
71
71
 
72
72