dce-reactkit 3.1.4 → 3.1.7

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.
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ import React from 'react';
7
7
  * @author Gabe Abrams
8
8
  */
9
9
 
10
- declare type Props$c = {
10
+ declare type Props$d = {
11
11
  children: React.ReactNode;
12
12
  sendRequest: SendRequestFunction;
13
13
  dark?: boolean;
@@ -52,7 +52,7 @@ declare const confirm: (title: string, text: string) => Promise<boolean>;
52
52
  * @param [errorTitle] title of the error box
53
53
  */
54
54
  declare const showFatalError: (error: any, errorTitle?: string) => undefined;
55
- declare const AppWrapper: React.FC<Props$c>;
55
+ declare const AppWrapper: React.FC<Props$d>;
56
56
 
57
57
  /**
58
58
  * Loading spinner/indicator
@@ -65,12 +65,12 @@ declare const LoadingSpinner: () => JSX.Element;
65
65
  * @author Gabe Abrams
66
66
  */
67
67
 
68
- declare type Props$b = {
68
+ declare type Props$c = {
69
69
  error: any;
70
70
  title?: string;
71
71
  onClose?: () => void;
72
72
  };
73
- declare const ErrorBox: React.FC<Props$b>;
73
+ declare const ErrorBox: React.FC<Props$c>;
74
74
 
75
75
  /**
76
76
  * Bootstrap variants
@@ -136,7 +136,7 @@ declare enum ModalType {
136
136
  * @author Gabe Abrams
137
137
  */
138
138
 
139
- declare type Props$a = {
139
+ declare type Props$b = {
140
140
  type?: ModalType;
141
141
  size?: ModalSize;
142
142
  title?: React.ReactNode;
@@ -165,27 +165,27 @@ declare type Props$a = {
165
165
  confirmVariant?: Variant;
166
166
  onTopOfOtherModals?: boolean;
167
167
  };
168
- declare const Modal: React.FC<Props$a>;
168
+ declare const Modal: React.FC<Props$b>;
169
169
 
170
170
  /**
171
171
  * A box with a tab on the top that holds buttons and other content
172
172
  * @author Gabe Abrams
173
173
  */
174
174
 
175
- declare type Props$9 = {
175
+ declare type Props$a = {
176
176
  title: React.ReactNode;
177
177
  children: React.ReactNode;
178
178
  noBottomMargin?: boolean;
179
179
  noBottomPadding?: boolean;
180
180
  };
181
- declare const TabBox: React.FC<Props$9>;
181
+ declare const TabBox: React.FC<Props$a>;
182
182
 
183
183
  /**
184
184
  * A radio selection button
185
185
  * @author Gabe Abrams
186
186
  */
187
187
 
188
- declare type Props$8 = {
188
+ declare type Props$9 = {
189
189
  text: string;
190
190
  onSelected: () => void;
191
191
  ariaLabel: string;
@@ -197,14 +197,14 @@ declare type Props$8 = {
197
197
  unselectedVariant?: Variant;
198
198
  small?: boolean;
199
199
  };
200
- declare const RadioButton: React.FC<Props$8>;
200
+ declare const RadioButton: React.FC<Props$9>;
201
201
 
202
202
  /**
203
203
  * A checkbox button
204
204
  * @author Gabe Abrams
205
205
  */
206
206
 
207
- declare type Props$7 = {
207
+ declare type Props$8 = {
208
208
  text: string;
209
209
  onChanged: (checked: boolean) => void;
210
210
  ariaLabel: string;
@@ -218,26 +218,26 @@ declare type Props$7 = {
218
218
  small?: boolean;
219
219
  dashed?: boolean;
220
220
  };
221
- declare const CheckboxButton: React.FC<Props$7>;
221
+ declare const CheckboxButton: React.FC<Props$8>;
222
222
 
223
223
  /**
224
224
  * Input group with a title and space for buttons
225
225
  * @author Gabe Abrams
226
226
  */
227
227
 
228
- declare type Props$6 = {
228
+ declare type Props$7 = {
229
229
  label: string;
230
230
  minLabelWidth?: string;
231
231
  children: React.ReactNode;
232
232
  };
233
- declare const ButtonInputGroup: React.FC<Props$6>;
233
+ declare const ButtonInputGroup: React.FC<Props$7>;
234
234
 
235
235
  /**
236
236
  * A very simple, lightweight date chooser
237
237
  * @author Gabe Abrams
238
238
  */
239
239
 
240
- declare type Props$5 = {
240
+ declare type Props$6 = {
241
241
  ariaLabel: string;
242
242
  name: string;
243
243
  month: number;
@@ -252,60 +252,60 @@ declare type Props$5 = {
252
252
  onChange: (month: number, day: number, year: number) => void;
253
253
  numMonthsToShow?: number;
254
254
  };
255
- declare const SimpleDateChooser: React.FC<Props$5>;
255
+ declare const SimpleDateChooser: React.FC<Props$6>;
256
256
 
257
257
  /**
258
258
  * Drawer container
259
259
  * @author Gabe Abrams
260
260
  */
261
261
 
262
- declare type Props$4 = {
262
+ declare type Props$5 = {
263
263
  children: React.ReactNode;
264
264
  };
265
- declare const Drawer: React.FC<Props$4>;
265
+ declare const Drawer: React.FC<Props$5>;
266
266
 
267
267
  /**
268
268
  * Success checkmark that pops into view
269
269
  * @author Gabe Abrams
270
270
  */
271
271
 
272
- declare type Props$3 = {
272
+ declare type Props$4 = {
273
273
  sizeRem?: number;
274
274
  circleVariant?: string;
275
275
  checkVariant?: string;
276
276
  };
277
- declare const PopSuccessMark: React.FC<Props$3>;
277
+ declare const PopSuccessMark: React.FC<Props$4>;
278
278
 
279
279
  /**
280
280
  * Failure x mark that pops into view
281
281
  * @author Gabe Abrams
282
282
  */
283
283
 
284
- declare type Props$2 = {
284
+ declare type Props$3 = {
285
285
  sizeRem?: number;
286
286
  circleVariant?: string;
287
287
  xVariant?: string;
288
288
  };
289
- declare const PopFailureMark: React.FC<Props$2>;
289
+ declare const PopFailureMark: React.FC<Props$3>;
290
290
 
291
291
  /**
292
292
  * Failure pending that pops into view
293
293
  * @author Gabe Abrams
294
294
  */
295
295
 
296
- declare type Props$1 = {
296
+ declare type Props$2 = {
297
297
  sizeRem?: number;
298
298
  circleVariant?: string;
299
299
  hourglassVariant?: string;
300
300
  };
301
- declare const PopPendingMark: React.FC<Props$1>;
301
+ declare const PopPendingMark: React.FC<Props$2>;
302
302
 
303
303
  /**
304
304
  * Copiable text box
305
305
  * @author Gabe Abrams
306
306
  */
307
307
 
308
- declare type Props = {
308
+ declare type Props$1 = {
309
309
  text: string;
310
310
  maxTextWidthRem?: number;
311
311
  label: string;
@@ -317,7 +317,40 @@ declare type Props = {
317
317
  textAreaId?: string;
318
318
  copyButtonId?: string;
319
319
  };
320
- declare const CopiableBox: React.FC<Props>;
320
+ declare const CopiableBox: React.FC<Props$1>;
321
+
322
+ /**
323
+ * An item that can be chosen (for use within ItemPicker)
324
+ * @author Gabe Abrams
325
+ */
326
+ declare type PickableItem = ({
327
+ id: number | string;
328
+ name: string;
329
+ link?: string;
330
+ } & ({
331
+ isGroup: false;
332
+ checked: boolean;
333
+ } | {
334
+ isGroup: true;
335
+ children: PickableItem[];
336
+ }));
337
+
338
+ /**
339
+ * Reusable nested item picker
340
+ * @author Yuen Ler Chow
341
+ */
342
+
343
+ declare type Props = {
344
+ title: string;
345
+ items: PickableItem[];
346
+ /**
347
+ * Handler to call when item selection is changed
348
+ * @param updatedItems an updated copy of the list of items with checked
349
+ * values updated
350
+ */
351
+ onChanged: (updatedItems: PickableItem[]) => void;
352
+ };
353
+ declare const ItemPicker: React.FC<Props>;
321
354
 
322
355
  /**
323
356
  * An error with a code
@@ -622,6 +655,18 @@ declare const stringsToHumanReadableList: (items: string[]) => string;
622
655
  */
623
656
  declare const onlyKeepLetters: (str: string) => string;
624
657
 
658
+ /**
659
+ * Run tasks in parallel with a limit on how many tasks can execute at once.
660
+ * No guarantees are made about the order of task execution
661
+ * @author Gabe Abrams
662
+ * @param taskFunctions functions that start asynchronous tasks and optionally
663
+ * resolve with values
664
+ * @param [limit=no limit] maximum number of asynchronous tasks to permit to run at
665
+ * once
666
+ * @returns array of resolved values in the same order as the task functions
667
+ */
668
+ declare const parallelLimit: (taskFunctions: (() => Promise<any>)[], limit?: number | undefined) => Promise<any[]>;
669
+
625
670
  /**
626
671
  * List of error codes built into the react kit
627
672
  * @author Gabe Abrams
@@ -653,4 +698,4 @@ declare enum DayOfWeek {
653
698
  Sunday = "u"
654
699
  }
655
700
 
656
- export { AppWrapper, ButtonInputGroup, CheckboxButton, CopiableBox, DAY_IN_MS, DayOfWeek, Drawer, ErrorBox, ErrorWithCode, HOUR_IN_MS, LoadingSpinner, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, Variant, abbreviate, alert, avg, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genRouteHandler, getHumanReadableDate, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, initServer, onlyKeepLetters, padDecimalZeros, padZerosLeft, roundToNumDecimals, showFatalError, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
701
+ export { AppWrapper, ButtonInputGroup, CheckboxButton, CopiableBox, DAY_IN_MS, DayOfWeek, Drawer, ErrorBox, ErrorWithCode, HOUR_IN_MS, ItemPicker, LoadingSpinner, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, Variant, abbreviate, alert, avg, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genRouteHandler, getHumanReadableDate, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, initServer, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, roundToNumDecimals, showFatalError, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.1.4",
3
+ "version": "3.1.7",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
- "build": "rm -rf dist && rollup -c"
9
+ "build": "rm -rf dist && rollup -c",
10
+ "test": "jest"
11
+ },
12
+ "jest": {
13
+ "preset": "ts-jest",
14
+ "testEnvironment": "jsdom"
10
15
  },
11
16
  "repository": {
12
17
  "type": "git",
@@ -30,6 +35,7 @@
30
35
  "@rollup/plugin-json": "^4.1.0",
31
36
  "@rollup/plugin-node-resolve": "^13.2.1",
32
37
  "@rollup/plugin-typescript": "^8.3.2",
38
+ "@types/jest": "^28.1.7",
33
39
  "@types/react": "^17.0.7",
34
40
  "@typescript-eslint/eslint-plugin": "^5.21.0",
35
41
  "@typescript-eslint/parser": "^5.21.0",
@@ -40,9 +46,13 @@
40
46
  "eslint-plugin-jsx-a11y": "^6.5.1",
41
47
  "eslint-plugin-react": "^7.29.4",
42
48
  "eslint-plugin-react-hooks": "^4.4.0",
49
+ "jest": "^28.1.3",
50
+ "jest-environment-jsdom": "^28.1.3",
51
+ "raixa": "^1.0.19",
43
52
  "rollup": "^2.70.2",
44
53
  "rollup-plugin-dts": "^4.2.1",
45
54
  "rollup-plugin-sourcemaps": "^0.6.3",
55
+ "ts-jest": "^28.0.8",
46
56
  "typescript": "^4.6.3"
47
57
  }
48
58
  }
package/sandbox.js ADDED
@@ -0,0 +1,20 @@
1
+ const { parallelLimit, waitMs } = require('.');
2
+
3
+ (async () => {
4
+
5
+ const tasks = [2, 4, 3, 5, 6, 20, 1, 1, 1, 1, 5, 4, 6].map((sec, i) => {
6
+ return async () => {
7
+ let prefix = '';
8
+ for (let j = 0; j < i; j++) {
9
+ prefix += '\t';
10
+ }
11
+ for (let j = 0; j < sec * 5; j++) {
12
+ await waitMs(200);
13
+ console.log(`${prefix}${j}/${sec * 5}`);
14
+ }
15
+ return i;
16
+ };
17
+ });
18
+
19
+ const results = await parallelLimit(tasks, 1000);
20
+ })();
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Reusable nested item picker
3
+ * @author Yuen Ler Chow
4
+ */
5
+
6
+ // Import React
7
+ import React, { useReducer } from 'react';
8
+
9
+ // Import FontAwesome
10
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11
+ import {
12
+ faChevronRight,
13
+ faChevronDown,
14
+ } from '@fortawesome/free-solid-svg-icons';
15
+
16
+ // Import shared components
17
+ import CheckboxButton from '../CheckboxButton';
18
+ import Variant from '../../types/Variant';
19
+
20
+ // Import types
21
+ import PickableItem from './types/PickableItem';
22
+
23
+ /*------------------------------------------------------------------------*/
24
+ /* Types */
25
+ /*------------------------------------------------------------------------*/
26
+
27
+ // Props definition
28
+ type Props = {
29
+ // List of items to show
30
+ items: PickableItem[],
31
+ /**
32
+ * Handler to call when item selection is changed
33
+ * @param updatedItems an updated copy of the list of items with checked
34
+ * values updated
35
+ */
36
+ onChanged: (updatedItems: PickableItem[]) => void,
37
+ };
38
+
39
+ /*------------------------------------------------------------------------*/
40
+ /* State */
41
+ /*------------------------------------------------------------------------*/
42
+
43
+ /* -------- State Definition -------- */
44
+
45
+ type State = {
46
+ // True if children are being shown
47
+ isShowingItems: boolean,
48
+ };
49
+
50
+ /* ------------- Actions ------------ */
51
+
52
+ // Types of actions
53
+ enum ActionType {
54
+ // Toggle whether the children are being shown
55
+ ToggleItems = 'toggle-items',
56
+ }
57
+
58
+ // Action definitions
59
+ type Action = (
60
+ | {
61
+ // Action type
62
+ type: ActionType.ToggleItems,
63
+ }
64
+ );
65
+
66
+ /**
67
+ * Reducer that executes actions
68
+ * @author Yuen Ler Chow
69
+ * @param state current state
70
+ * @param action action to execute
71
+ */
72
+ const reducer = (state: State, action: Action): State => {
73
+ switch (action.type) {
74
+ case ActionType.ToggleItems: {
75
+ return { isShowingItems: !state.isShowingItems };
76
+ }
77
+ default: {
78
+ return state;
79
+ }
80
+ }
81
+ };
82
+
83
+ /*------------------------------------------------------------------------*/
84
+ /* Component */
85
+ /*------------------------------------------------------------------------*/
86
+
87
+ const NestableItemList: React.FC<Props> = (props) => {
88
+ /*------------------------------------------------------------------------*/
89
+ /* Setup */
90
+ /*------------------------------------------------------------------------*/
91
+
92
+ /* -------------- Props ------------- */
93
+
94
+ // Destructure all props
95
+ const {
96
+ items,
97
+ onChanged,
98
+ } = props;
99
+
100
+ /* -------------- State ------------- */
101
+
102
+ // Initial state
103
+ const initialState: State = {
104
+ isShowingItems: true,
105
+ };
106
+
107
+ // Initialize state
108
+ const [state, dispatch] = useReducer(reducer, initialState);
109
+
110
+ // Destructure common state
111
+ const {
112
+ isShowingItems,
113
+ } = state;
114
+
115
+ /*------------------------------------------------------------------------*/
116
+ /* Component Functions */
117
+ /*------------------------------------------------------------------------*/
118
+
119
+ /**
120
+ * Checks if all items in a list are checked
121
+ * @author Yuen Ler Chow
122
+ * @param pickableItems a list of items
123
+ * @returns true if all items are checked. If any item is a group,
124
+ * we recursively check its children
125
+ */
126
+ const allChecked = (pickableItems: PickableItem[]): boolean => {
127
+ return pickableItems.every((item: PickableItem) => {
128
+ if (item.isGroup) {
129
+ return allChecked(item.children);
130
+ }
131
+ return item.checked;
132
+ });
133
+ };
134
+
135
+ /**
136
+ * Checks if none of the items in a list are checked
137
+ * @author Yuen Ler Chow
138
+ * @param pickableItems a list of items
139
+ * @returns true if all items are unchecked. If any item is a group, we
140
+ * recursively check its children
141
+ */
142
+ const noneChecked = (pickableItems: PickableItem[]): boolean => {
143
+ return pickableItems.every((item: PickableItem) => {
144
+ if (item.isGroup) {
145
+ return noneChecked(item.children);
146
+ }
147
+ return !item.checked;
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Change whether specific items are checked
153
+ * @author Yuen Ler Chow
154
+ * @param id the id of the item we want to check or uncheck OR true if
155
+ * we are checking/unchecking all items, independent of their id
156
+ * @param checked if true, the item will be checked.
157
+ * @param pickableItems the list of items we iterate through to find the item
158
+ * we want to check/uncheck
159
+ * @returns a list of items with the item now checked/unchecked.
160
+ * If it is a group, its children will also become checked/unchecked
161
+ */
162
+ const changeChecked = (id: string | number | true, checked: boolean, pickableItems: PickableItem[]): PickableItem[] => {
163
+ const updatedItems: PickableItem[] = pickableItems.map((item: PickableItem) => {
164
+ if (item.id === id || id === true) {
165
+ if (item.isGroup) {
166
+ return {
167
+ ...item,
168
+ children: changeChecked(true, checked, item.children),
169
+ };
170
+ }
171
+ return {
172
+ ...item,
173
+ checked,
174
+ };
175
+ }
176
+ if (item.isGroup) {
177
+ return {
178
+ ...item,
179
+ children: changeChecked(id, checked, item.children),
180
+ };
181
+ }
182
+ return item;
183
+ });
184
+ return updatedItems;
185
+ };
186
+
187
+ /**
188
+ * Within a tree of items, swap in the list of updated items into the spot
189
+ * indicated by the id
190
+ * @author Yuen Ler Chow
191
+ * @param id the id of the item group we want to change
192
+ * @param updatedItems the list of items we want to change the items in the group to
193
+ * @returns a list of all items containing the updated items
194
+ */
195
+ const changeItems = (id: string | number, updatedItems: PickableItem[]): PickableItem[] => {
196
+ return items.map((item: PickableItem) => {
197
+ if (item.id === id) {
198
+ return {
199
+ ...item,
200
+ children: updatedItems,
201
+ };
202
+ }
203
+ return item;
204
+ });
205
+ };
206
+
207
+ /*------------------------------------------------------------------------*/
208
+ /* Render */
209
+ /*------------------------------------------------------------------------*/
210
+
211
+ return (
212
+ <div>
213
+ {
214
+ items.map((item) => {
215
+ return (
216
+ <div key={item.id}>
217
+ {/* Dropdown Button */}
218
+ <span
219
+ className="NestableItemList-dropdown-button-container d-inline-block"
220
+ style={{
221
+ minWidth: '2rem',
222
+ }}
223
+ >
224
+ {item.isGroup && (
225
+ <button
226
+ className={`NestableItemList-dropdown-button NestableItemList-dropdown-button-${item.id}`}
227
+ style={{
228
+ border: 0,
229
+ backgroundColor: 'transparent',
230
+ }}
231
+ type="button"
232
+ onClick={() => {
233
+ dispatch({
234
+ type: ActionType.ToggleItems,
235
+ });
236
+ }}
237
+ aria-label={`${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}`}
238
+ >
239
+ <FontAwesomeIcon
240
+ icon={isShowingItems ? faChevronDown : faChevronRight}
241
+ />
242
+ </button>
243
+ )}
244
+ </span>
245
+
246
+ {/* Checkbox and Text */}
247
+ <CheckboxButton
248
+ className={`NestableItemList-CheckboxButton-${item.id}`}
249
+ text={item.name}
250
+ checked={item.isGroup ? allChecked(item.children) : item.checked}
251
+ dashed={item.isGroup ? !noneChecked(item.children) : false}
252
+ onChanged={(checked) => {
253
+ onChanged(changeChecked(item.id, checked, items));
254
+ }}
255
+ ariaLabel={`Select ${item.name}`}
256
+ checkedVariant={Variant.Light}
257
+ />
258
+
259
+ {/* Children */}
260
+ {item.isGroup && isShowingItems && (
261
+ <div
262
+ className="NestableItemList-children-container"
263
+ style={{
264
+ paddingLeft: '2.2rem',
265
+ }}
266
+ >
267
+ <NestableItemList
268
+ items={item.children}
269
+ onChanged={(updatedItems) => {
270
+ onChanged(changeItems(item.id, updatedItems));
271
+ }}
272
+ />
273
+ </div>
274
+ )}
275
+ </div>
276
+ );
277
+ })
278
+ }
279
+ </div>
280
+ );
281
+ };
282
+
283
+ /*------------------------------------------------------------------------*/
284
+ /* Wrap Up */
285
+ /*------------------------------------------------------------------------*/
286
+
287
+ // Export component
288
+ export default NestableItemList;