dce-reactkit 4.0.0-beta.3 → 4.0.0

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.
@@ -14,6 +14,7 @@ type Props = {
14
14
  */
15
15
  onChanged: (updatedItems: PickableItem[]) => void;
16
16
  noBottomMargin?: boolean;
17
+ hideSelectAllOrNoneButtons?: boolean;
17
18
  };
18
19
  declare const ItemPicker: React.FC<Props>;
19
20
  export default ItemPicker;
@@ -38,5 +38,7 @@ type ModalProps = {
38
38
  confirmLabel?: string;
39
39
  confirmVariant?: Variant;
40
40
  onTopOfOtherModals?: boolean;
41
+ isLoading?: boolean;
42
+ isLoadingCancelable?: boolean;
41
43
  };
42
44
  export default ModalProps;
@@ -6,6 +6,7 @@ import React from 'react';
6
6
  type Props = {
7
7
  title: React.ReactNode;
8
8
  children: React.ReactNode;
9
+ topRightChildren?: React.ReactNode;
9
10
  noBottomMargin?: boolean;
10
11
  noBottomPadding?: boolean;
11
12
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Get number of words in string
3
+ * @author Gardenia Liu
4
+ * @author Allison Zhang
5
+ * @author Gabe Abrams
6
+ * @param text the string to check
7
+ * @returns number of words in the string
8
+ */
9
+ declare const getWordCount: (text: string) => number;
10
+ export default getWordCount;
@@ -75,6 +75,7 @@ import mapAsync from './helpers/asyncArrayFunctions/mapAsync';
75
75
  import someAsync from './helpers/asyncArrayFunctions/someAsync';
76
76
  import capitalize from './helpers/capitalize';
77
77
  import shuffleArray from './helpers/shuffleArray';
78
+ import getWordCount from './helpers/getWordCount';
78
79
  import ParamType from './types/ParamType';
79
80
  import ModalButtonType from './types/ModalButtonType';
80
81
  import ModalSize from './types/ModalSize';
@@ -99,4 +100,4 @@ import PickableItem from './components/ItemPicker/types/PickableItem';
99
100
  import DBEntry from './components/DBEntryManagerPanel/types/DBEntry';
100
101
  import DBEntryField from './components/DBEntryManagerPanel/types/DBEntryField';
101
102
  import DBEntryFieldType from './components/DBEntryManagerPanel/types/DBEntryFieldType';
102
- export { AppWrapper, LoadingSpinner, ErrorBox, Modal, TabBox, RadioButton, CheckboxButton, ButtonInputGroup, SimpleDateChooser, Drawer, PopSuccessMark, PopFailureMark, PopPendingMark, CopiableBox, ItemPicker, LogReviewer, IntelliTable, CSVDownloadButton, DBEntryManagerPanel, Tooltip, ToggleSwitch, AutoscrollToBottomContainer, MultiSwitch, Dropdown, alert, prompt, confirm, showFatalError, ErrorWithCode, MINUTE_IN_MS, HOUR_IN_MS, DAY_IN_MS, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_ROUTE_PATH, LOG_REVIEW_STATUS_ROUTE, DynamicWord, abbreviate, avg, ceilToNumDecimals, floorToNumDecimals, forceNumIntoBounds, padDecimalZeros, padZerosLeft, roundToNumDecimals, sum, waitMs, getOrdinal, getTimeInfoInET, stubServerEndpoint, startMinWait, getHumanReadableDate, getPartOfDay, stringsToHumanReadableList, onlyKeepLetters, parallelLimit, getMonthName, genCSV, canReviewLogs, isMobileOrTablet, extractProp, compareArraysByProp, genCommaList, validateEmail, validatePhoneNumber, validateString, getLocalTimeInfo, idify, makeLinksClickable, prefixWithAOrAn, everyAsync, filterAsync, forEachAsync, mapAsync, someAsync, capitalize, shuffleArray, initClient, visitServerEndpoint, logClientEvent, addFatalErrorHandler, leaveToURL, combineClassNames, useForceRender, setClientEventMetadataPopulator, ParamType, ModalButtonType, ModalSize, ModalType, ReactKitErrorCode, Variant, DayOfWeek, Log, LogType, LogSource, LogAction, LogBuiltInMetadata, LogMetadataType, LogFunction, LogTypeSpecificInfo, LogMainInfo, LogSourceSpecificInfo, LogLevel, IntelliTableColumn, DropdownItemType, PickableItem, DBEntry, DBEntryField, DBEntryFieldType, };
103
+ export { AppWrapper, LoadingSpinner, ErrorBox, Modal, TabBox, RadioButton, CheckboxButton, ButtonInputGroup, SimpleDateChooser, Drawer, PopSuccessMark, PopFailureMark, PopPendingMark, CopiableBox, ItemPicker, LogReviewer, IntelliTable, CSVDownloadButton, DBEntryManagerPanel, Tooltip, ToggleSwitch, AutoscrollToBottomContainer, MultiSwitch, Dropdown, alert, prompt, confirm, showFatalError, ErrorWithCode, MINUTE_IN_MS, HOUR_IN_MS, DAY_IN_MS, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_ROUTE_PATH, LOG_REVIEW_STATUS_ROUTE, DynamicWord, abbreviate, avg, ceilToNumDecimals, floorToNumDecimals, forceNumIntoBounds, padDecimalZeros, padZerosLeft, roundToNumDecimals, sum, waitMs, getOrdinal, getTimeInfoInET, stubServerEndpoint, startMinWait, getHumanReadableDate, getPartOfDay, stringsToHumanReadableList, onlyKeepLetters, parallelLimit, getMonthName, genCSV, canReviewLogs, isMobileOrTablet, extractProp, compareArraysByProp, genCommaList, validateEmail, validatePhoneNumber, validateString, getLocalTimeInfo, idify, makeLinksClickable, prefixWithAOrAn, everyAsync, filterAsync, forEachAsync, mapAsync, someAsync, capitalize, shuffleArray, getWordCount, initClient, visitServerEndpoint, logClientEvent, addFatalErrorHandler, leaveToURL, combineClassNames, useForceRender, setClientEventMetadataPopulator, ParamType, ModalButtonType, ModalSize, ModalType, ReactKitErrorCode, Variant, DayOfWeek, Log, LogType, LogSource, LogAction, LogBuiltInMetadata, LogMetadataType, LogFunction, LogTypeSpecificInfo, LogMainInfo, LogSourceSpecificInfo, LogLevel, IntelliTableColumn, DropdownItemType, PickableItem, DBEntry, DBEntryField, DBEntryFieldType, };
package/dist/index.d.ts CHANGED
@@ -209,6 +209,8 @@ type ModalProps = {
209
209
  confirmLabel?: string;
210
210
  confirmVariant?: Variant;
211
211
  onTopOfOtherModals?: boolean;
212
+ isLoading?: boolean;
213
+ isLoadingCancelable?: boolean;
212
214
  };
213
215
 
214
216
  /**
@@ -227,6 +229,7 @@ declare const Modal: React$1.FC<ModalProps>;
227
229
  type Props$j = {
228
230
  title: React$1.ReactNode;
229
231
  children: React$1.ReactNode;
232
+ topRightChildren?: React$1.ReactNode;
230
233
  noBottomMargin?: boolean;
231
234
  noBottomPadding?: boolean;
232
235
  };
@@ -412,6 +415,7 @@ type Props$9 = {
412
415
  */
413
416
  onChanged: (updatedItems: PickableItem[]) => void;
414
417
  noBottomMargin?: boolean;
418
+ hideSelectAllOrNoneButtons?: boolean;
415
419
  };
416
420
  declare const ItemPicker: React$1.FC<Props$9>;
417
421
 
@@ -1498,6 +1502,16 @@ declare const capitalize: (str: string) => string;
1498
1502
  */
1499
1503
  declare const shuffleArray: <T>(arr: T[]) => T[];
1500
1504
 
1505
+ /**
1506
+ * Get number of words in string
1507
+ * @author Gardenia Liu
1508
+ * @author Allison Zhang
1509
+ * @author Gabe Abrams
1510
+ * @param text the string to check
1511
+ * @returns number of words in the string
1512
+ */
1513
+ declare const getWordCount: (text: string) => number;
1514
+
1501
1515
  /**
1502
1516
  * Days of the week
1503
1517
  * @author Gabe Abrams
@@ -1539,4 +1553,4 @@ declare enum ReactKitErrorCode {
1539
1553
  NoCACCLSendRequestFunction = "DRK7"
1540
1554
  }
1541
1555
 
1542
- export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntry, DBEntryField, DBEntryFieldType, DBEntryManagerPanel, DayOfWeek, Drawer, Dropdown, DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, IntelliTableColumn, ItemPicker, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_REVIEW_STATUS_ROUTE, LOG_ROUTE_PATH, LoadingSpinner, Log, LogAction, LogBuiltInMetadata, LogFunction, LogLevel, LogMainInfo, LogMetadataType, LogReviewer, LogSource, LogSourceSpecificInfo, LogType, LogTypeSpecificInfo, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, MultiSwitch, ParamType, PickableItem, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant, abbreviate, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, idify, initClient, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitServerEndpoint, waitMs };
1556
+ export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntry, DBEntryField, DBEntryFieldType, DBEntryManagerPanel, DayOfWeek, Drawer, Dropdown, DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, IntelliTableColumn, ItemPicker, LOG_REVIEW_ROUTE_PATH_PREFIX, LOG_REVIEW_STATUS_ROUTE, LOG_ROUTE_PATH, LoadingSpinner, Log, LogAction, LogBuiltInMetadata, LogFunction, LogLevel, LogMainInfo, LogMetadataType, LogReviewer, LogSource, LogSourceSpecificInfo, LogType, LogTypeSpecificInfo, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, MultiSwitch, ParamType, PickableItem, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant, abbreviate, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, getWordCount, idify, initClient, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitServerEndpoint, waitMs };
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0",
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
9
  "build": "rm -rf dist && rollup -c",
10
- "test": "jest --runInBand",
11
- "gen-reactkit-cross-server-secret": "npx tsx genEncodedSecret.ts"
10
+ "test": "jest --runInBand"
12
11
  },
13
12
  "jest": {
14
13
  "preset": "ts-jest",
package/rollup.config.js CHANGED
@@ -27,7 +27,6 @@ export default [
27
27
  typescript({ tsconfig: "./tsconfig.json" }),
28
28
  sourcemaps(),
29
29
  ],
30
- inlineDynamicImports: true,
31
30
  external: [
32
31
  ...Object.keys(packageJson.dependencies || {}),
33
32
  ...Object.keys(packageJson.peerDependencies || {}),
@@ -37,7 +36,6 @@ export default [
37
36
  input: "dist/esm/types/index.d.ts",
38
37
  output: [{ file: "dist/index.d.ts", format: "esm" }],
39
38
  plugins: [dts()],
40
- inlineDynamicImports: true,
41
39
  external: [
42
40
  ...Object.keys(packageJson.dependencies || {}),
43
41
  ...Object.keys(packageJson.peerDependencies || {}),
@@ -315,7 +315,7 @@ const IntelliTable: React.FC<Props> = (props) => {
315
315
  checked={columnVisibilityMap[column.param]}
316
316
  ariaLabel={`show "${column.title}" column in the ${title} table`}
317
317
  checkedVariant={Variant.Light}
318
- uncheckedVariant={Variant.Secondary}
318
+ uncheckedVariant={Variant.Light}
319
319
  />
320
320
  );
321
321
  })
@@ -272,6 +272,7 @@ const NestableItemList: React.FC<Props> = (props) => {
272
272
  }}
273
273
  ariaLabel={`Select ${item.name}`}
274
274
  checkedVariant={Variant.Light}
275
+ uncheckedVariant={Variant.Light}
275
276
  />
276
277
 
277
278
  {/* Children */}
@@ -33,6 +33,8 @@ type Props = {
33
33
  onChanged: (updatedItems: PickableItem[]) => void,
34
34
  // If true, don't add margin to bottom of item picker
35
35
  noBottomMargin?: boolean,
36
+ // If true, hide select all or none buttons
37
+ hideSelectAllOrNoneButtons?: boolean,
36
38
  };
37
39
 
38
40
  /*------------------------------------------------------------------------*/
@@ -52,12 +54,61 @@ const ItemPicker: React.FC<Props> = (props) => {
52
54
  items,
53
55
  onChanged,
54
56
  noBottomMargin,
57
+ hideSelectAllOrNoneButtons,
55
58
  } = props;
56
59
 
57
60
  /*------------------------------------------------------------------------*/
58
61
  /* ------------------------- Component Functions ------------------------ */
59
62
  /*------------------------------------------------------------------------*/
60
63
 
64
+ /**
65
+ * Updates the checked state of an item (including all children)
66
+ * @author Yuen Ler Chow
67
+ * @param item the item to update
68
+ * @param checked the new checked state
69
+ * @returns the updated item
70
+ */
71
+ const updateItemChecked = (
72
+ item: PickableItem,
73
+ checked: boolean,
74
+ ): PickableItem => {
75
+ if (item.isGroup) {
76
+ return {
77
+ ...item,
78
+ children: item.children.map((child) => {
79
+ return updateItemChecked(child, checked);
80
+ }),
81
+ };
82
+ }
83
+ return { ...item, checked };
84
+ };
85
+
86
+ /**
87
+ * Selects all items in the list
88
+ * @author Yuen Ler Chow
89
+ */
90
+ const handleSelectAll = () => {
91
+ const updatedItems = items.map(
92
+ (item) => {
93
+ return updateItemChecked(item, true);
94
+ },
95
+ );
96
+ onChanged(updatedItems);
97
+ };
98
+
99
+ /**
100
+ * Deselects all items in the list
101
+ * @author Yuen Ler Chow
102
+ */
103
+ const handleDeselectAll = () => {
104
+ const updatedItems = items.map(
105
+ (item) => {
106
+ return updateItemChecked(item, false);
107
+ },
108
+ );
109
+ onChanged(updatedItems);
110
+ };
111
+
61
112
  /*------------------------------------------------------------------------*/
62
113
  /* ------------------------------- Render ------------------------------- */
63
114
  /*------------------------------------------------------------------------*/
@@ -66,10 +117,55 @@ const ItemPicker: React.FC<Props> = (props) => {
66
117
  /* --------------- Main UI -------------- */
67
118
  /*----------------------------------------*/
68
119
 
120
+ // Select all/none
121
+ const selectAllOrNone = (
122
+ !hideSelectAllOrNoneButtons
123
+ ? (
124
+ <div
125
+ className="d-flex h-100 align-items-end flex-row"
126
+ >
127
+ <div className="d-flex justify-content-end">
128
+ {/* Label */}
129
+ <div
130
+ className="me-2"
131
+ style={{ fontSize: '1.2rem' }}
132
+ >
133
+ Select
134
+ </div>
135
+ {/* Buttons */}
136
+ <div className="btn-group" role="group">
137
+ {/* All */}
138
+ <button
139
+ type="button"
140
+ style={{ borderRight: '0.1rem solid white' }}
141
+ aria-label="Select all contexts"
142
+ className="btn btn-secondary py-0"
143
+ onClick={handleSelectAll}
144
+ >
145
+ All
146
+ </button>
147
+ {/* None */}
148
+ <button
149
+ type="button"
150
+ aria-label="Deselect all contexts"
151
+ className="btn btn-secondary py-0"
152
+ onClick={handleDeselectAll}
153
+ >
154
+ None
155
+ </button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ )
160
+ : undefined
161
+ );
162
+
163
+ // Main UI
69
164
  return (
70
165
  <TabBox
71
166
  title={title}
72
167
  noBottomMargin={noBottomMargin}
168
+ topRightChildren={selectAllOrNone}
73
169
  >
74
170
  <div style={{ overflowX: 'auto' }}>
75
171
  <NestableItemList
@@ -69,6 +69,10 @@ type ModalProps = {
69
69
  confirmVariant?: Variant,
70
70
  // True if modal should be on top of other modals
71
71
  onTopOfOtherModals?: boolean,
72
+ // If true, the modal is loading
73
+ isLoading?: boolean,
74
+ // If true, the loading is cancelable
75
+ isLoadingCancelable?: boolean,
72
76
  };
73
77
 
74
78
  export default ModalProps;
@@ -14,6 +14,9 @@ import React, { useState, useEffect, useRef } from 'react';
14
14
  import ReactDOM from 'react-dom';
15
15
 
16
16
  // Import other components
17
+ import LoadingSpinner from '../LoadingSpinner';
18
+
19
+ // Import helpers
17
20
  import waitMs from '../../helpers/waitMs';
18
21
 
19
22
  // Import types
@@ -41,6 +44,7 @@ const BASE_Z_INDEX_ON_TOP = 2000000000;
41
44
 
42
45
  // Constants
43
46
  const MS_TO_ANIMATE = 200; // Animation duration
47
+ const MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR = 1000;
44
48
 
45
49
  // Modal type to list of buttons
46
50
  const modalTypeToModalButtonTypes: {
@@ -256,6 +260,8 @@ const Modal: React.FC<ModalProps> = (props) => {
256
260
  dontAllowBackdropExit,
257
261
  dontShowXButton,
258
262
  onTopOfOtherModals,
263
+ isLoading,
264
+ isLoadingCancelable,
259
265
  } = props;
260
266
 
261
267
  // Determine if no header either
@@ -266,6 +272,7 @@ const Modal: React.FC<ModalProps> = (props) => {
266
272
  // True if animation is in use
267
273
  const [animatingIn, setAnimatingIn] = useState(true);
268
274
  const [animatingPop, setAnimatingPop] = useState(false);
275
+ const [showModal, setShowModal] = useState(false);
269
276
 
270
277
  /* -------------- Refs -------------- */
271
278
 
@@ -298,6 +305,15 @@ const Modal: React.FC<ModalProps> = (props) => {
298
305
  if (mounted.current) {
299
306
  setAnimatingIn(false);
300
307
  }
308
+ if (isLoading) {
309
+ // wait before showing modal
310
+ await waitMs(MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR);
311
+ if (mounted.current) {
312
+ setShowModal(true);
313
+ }
314
+ } else {
315
+ setShowModal(true);
316
+ }
301
317
  })();
302
318
 
303
319
  return () => {
@@ -400,6 +416,17 @@ const Modal: React.FC<ModalProps> = (props) => {
400
416
  animationClass = 'Modal-animating-pop';
401
417
  }
402
418
 
419
+ // default to show close button when not loading and not show close button when loading
420
+ // unless downShowXButton or isLoadingCancelable
421
+ const showCloseButton = (
422
+ // The modal must have an onClose function
423
+ onClose
424
+ // ...and the close button is allowed to be visible
425
+ && !dontShowXButton
426
+ // ...and should not be shown if the modal is loading and not cancelable
427
+ && !(isLoading && !isLoadingCancelable)
428
+ );
429
+
403
430
  // Render the modal
404
431
  const contentToRender = (
405
432
  <div
@@ -436,6 +463,8 @@ const Modal: React.FC<ModalProps> = (props) => {
436
463
  handleClose(ModalButtonType.Cancel);
437
464
  }}
438
465
  />
466
+
467
+ {showModal && (
439
468
  <div
440
469
  className={`modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`}
441
470
  style={{
@@ -498,7 +527,7 @@ const Modal: React.FC<ModalProps> = (props) => {
498
527
  {title}
499
528
  </h5>
500
529
 
501
- {(onClose && !dontShowXButton) && (
530
+ {showCloseButton && (
502
531
  <button
503
532
  type="button"
504
533
  className="Modal-x-button btn-close"
@@ -518,25 +547,34 @@ const Modal: React.FC<ModalProps> = (props) => {
518
547
  )}
519
548
  </div>
520
549
  )}
521
- {children && (
522
- <div
523
- className="modal-body"
524
- style={{
525
- color: (
526
- isDarkModeOn()
527
- ? 'white'
528
- : undefined
529
- ),
530
- backgroundColor: (
531
- isDarkModeOn()
532
- ? '#444'
533
- : undefined
534
- ),
535
- }}
536
- >
537
- {children}
538
- </div>
539
- )}
550
+
551
+ <div
552
+ className="modal-body"
553
+ style={{
554
+ color: (
555
+ isDarkModeOn()
556
+ ? 'white'
557
+ : undefined
558
+ ),
559
+ backgroundColor: (
560
+ isDarkModeOn()
561
+ ? '#444'
562
+ : undefined
563
+ ),
564
+ }}
565
+ >
566
+ {
567
+ isLoading
568
+ ? (
569
+ <>
570
+ <LoadingSpinner />
571
+ <span className="sr-only">Content loading</span>
572
+ </>
573
+ )
574
+ : children
575
+ }
576
+ </div>
577
+
540
578
  {footer && (
541
579
  <div
542
580
  className="modal-footer pt-1 pb-1"
@@ -563,6 +601,7 @@ const Modal: React.FC<ModalProps> = (props) => {
563
601
  )}
564
602
  </div>
565
603
  </div>
604
+ )}
566
605
  </div>
567
606
  );
568
607
 
@@ -15,6 +15,8 @@ type Props = {
15
15
  title: React.ReactNode,
16
16
  // Children/contents inside the box
17
17
  children: React.ReactNode,
18
+ // Children to display on the top right of the tab box
19
+ topRightChildren?: React.ReactNode,
18
20
  // If true, don't add margin below the tab box
19
21
  noBottomMargin?: boolean,
20
22
  // If true, don't add padding to bottom of tab box
@@ -49,7 +51,9 @@ const style = `
49
51
 
50
52
  /* Container for Title */
51
53
  .TabBox-title-container {
52
- /* Place on Left */
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
53
57
  position: relative;
54
58
  left: 0;
55
59
  text-align: left;
@@ -82,6 +86,19 @@ const style = `
82
86
  background: #fdfdfd;
83
87
  }
84
88
 
89
+ .TabBox-title-right-container {
90
+ display: flex;
91
+ flex-direction: row;
92
+ align-items: bottom;
93
+ height: 2.4rem;
94
+ overflow: visible;
95
+ }
96
+
97
+ .TabBox-title-right-contents {
98
+ margin-right: 0.5rem;
99
+ margin-bottom: 0.2rem;
100
+ }
101
+
85
102
  /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
86
103
  .TabBox-children {
87
104
  position: relative;
@@ -98,11 +115,10 @@ const TabBox: React.FC<Props> = (props) => {
98
115
  /* -------------------------------- Setup ------------------------------- */
99
116
  /*------------------------------------------------------------------------*/
100
117
 
101
- /* -------------- Props ------------- */
102
-
103
118
  const {
104
119
  title,
105
120
  children,
121
+ topRightChildren,
106
122
  noBottomPadding,
107
123
  noBottomMargin,
108
124
  } = props;
@@ -111,21 +127,23 @@ const TabBox: React.FC<Props> = (props) => {
111
127
  /* ------------------------------- Render ------------------------------- */
112
128
  /*------------------------------------------------------------------------*/
113
129
 
114
- /*----------------------------------------*/
115
- /* --------------- Main UI -------------- */
116
- /*----------------------------------------*/
117
-
118
- // Full UI
119
130
  return (
120
131
  <div className={`TabBox-container ${noBottomMargin ? '' : 'mb-2'}`}>
121
132
  {/* Style */}
122
133
  <style>{style}</style>
123
134
 
124
- {/* Title */}
135
+ {/* Title Row with Left and Right sections */}
125
136
  <div className="TabBox-title-container">
126
137
  <div className="TabBox-title">
127
138
  {title}
128
139
  </div>
140
+ {topRightChildren && (
141
+ <div className="TabBox-title-right-container">
142
+ <div className="TabBox-title-right-contents">
143
+ {topRightChildren}
144
+ </div>
145
+ </div>
146
+ )}
129
147
  </div>
130
148
 
131
149
  {/* Contents */}
@@ -0,0 +1,26 @@
1
+ const punctuationRegex = /[!@#$%^&*(),.?\/;:'"\-\[\]]/g;
2
+
3
+ /**
4
+ * Get number of words in string
5
+ * @author Gardenia Liu
6
+ * @author Allison Zhang
7
+ * @author Gabe Abrams
8
+ * @param text the string to check
9
+ * @returns number of words in the string
10
+ */
11
+ const getWordCount = (text: string): number => {
12
+ const trimmedTextWithoutPunctuation = (
13
+ text
14
+ // Remove leading and trailing whitespace
15
+ .trim()
16
+ // Remove punctuation
17
+ .replace(punctuationRegex, '')
18
+ );
19
+ if (trimmedTextWithoutPunctuation.length === 0) {
20
+ return 0;
21
+ }
22
+
23
+ return trimmedTextWithoutPunctuation.split(/\s+/g).length;
24
+ };
25
+
26
+ export default getWordCount;
package/src/index.ts CHANGED
@@ -91,6 +91,7 @@ import mapAsync from './helpers/asyncArrayFunctions/mapAsync';
91
91
  import someAsync from './helpers/asyncArrayFunctions/someAsync';
92
92
  import capitalize from './helpers/capitalize';
93
93
  import shuffleArray from './helpers/shuffleArray';
94
+ import getWordCount from './helpers/getWordCount';
94
95
 
95
96
  // Import types
96
97
  import ParamType from './types/ParamType';
@@ -204,6 +205,7 @@ export {
204
205
  someAsync,
205
206
  capitalize,
206
207
  shuffleArray,
208
+ getWordCount,
207
209
  // Client helpers
208
210
  initClient,
209
211
  visitServerEndpoint,
@@ -1,84 +0,0 @@
1
- // Import crypto lib
2
- import crypto from 'crypto';
3
-
4
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
5
-
6
- // Get args
7
- const REACTKIT_CRED_ENCODING_SALT = process.env.npm_config_salt;
8
- if (!REACTKIT_CRED_ENCODING_SALT) {
9
- console.log('Encoding salt is required: --salt=...');
10
- process.exit(1);
11
- }
12
-
13
- // Get the key
14
- let key = process.env.npm_config_key;
15
- if (!key) {
16
- console.log('Key is required: --key=...');
17
- process.exit(1);
18
- }
19
-
20
- // Get the description
21
- let description = process.env.npm_config_description;
22
- if (!description) {
23
- console.log('Description is required: --description=...');
24
- process.exit(1);
25
- }
26
-
27
- // Get secret
28
- let secret = process.env.npm_config_secret;
29
- if (!secret) {
30
- // Generate a random secret
31
- secret = '';
32
- for (let i = 0; i < 32; i++) {
33
- secret += chars.charAt(Math.floor(Math.random() * chars.length));
34
- }
35
- console.log('Generated a random secret. If you have one in mind, use --secret=...');
36
- }
37
-
38
- // Get the host name
39
- const host = process.env.npm_config_host;
40
- if (!host) {
41
- console.log('Host of the receiving server is required: --host=...');
42
- process.exit(1);
43
- }
44
-
45
- // Encryption process based on:
46
- // https://medium.com/@tony.infisical/guide-to-nodes-crypto-module-for-encryption-decryption-65c077176980
47
-
48
- // Create a random initialization vector
49
- const iv = crypto.randomBytes(12).toString('base64');
50
-
51
- // Create a cipher
52
- const cipher = crypto.createCipheriv(
53
- 'aes-256-gcm',
54
- Buffer.from(secret, 'base64'),
55
- Buffer.from(iv, 'base64'),
56
- );
57
-
58
- // Encrypt the string
59
- let ciphertext = cipher.update(secret, 'utf8', 'base64');
60
-
61
- // Finalize the encryption
62
- ciphertext += cipher.final('base64');
63
-
64
- // Get the authentication tag
65
- const tag = cipher.getAuthTag();
66
-
67
- // JSONify the encrypted data
68
- const encryptionPack = encodeURIComponent(JSON.stringify({
69
- ciphertext,
70
- iv,
71
- tag,
72
- }));
73
-
74
- // Show the encrypted data
75
- console.log('\n\n');
76
- console.log('––––– Done! What\'s Next: –––––');
77
- console.log('');
78
- console.log('On the server *sending* the requests, append the following to the REACTKIT_CROSS_SERVER_CREDENTIALS env var:');
79
- console.log(`|${host}:${key}:${secret}|`);
80
- console.log('');
81
- console.log('On the server *receiving* the requests, add an entry to the "CrossServerCredential" collection:');
82
- console.log(`{ "description": "${description}", "key": "${key}", "encodedeSecret": "${encryptionPack}", "scopes": [] }`);
83
- console.log('');
84
- console.log('For all scopes that the server should have access to, add them to the "scopes" array.');