dce-reactkit 3.9.0-beta-loading-modal.2 → 3.9.0-beta-loading-modal.3
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/cjs/index.js +783 -201
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/client/initClient.d.ts +16 -2
- package/dist/cjs/types/components/AppWrapper.d.ts +32 -0
- package/dist/cjs/types/components/CheckboxButton.d.ts +1 -0
- package/dist/cjs/types/components/Dropdown.d.ts +32 -0
- package/dist/cjs/types/components/RadioButton.d.ts +2 -0
- package/dist/cjs/types/helpers/genRouteHandler.d.ts +8 -0
- package/dist/cjs/types/helpers/validators/ChicagoTitleCase.d.ts +9 -0
- package/dist/cjs/types/helpers/validators/validURL.d.ts +8 -0
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +24 -0
- package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +33 -0
- package/dist/cjs/types/index.d.ts +5 -2
- package/dist/cjs/types/types/DropdownItemType.d.ts +6 -0
- package/dist/cjs/types/types/ModalType.d.ts +1 -0
- package/dist/cjs/types/types/ReactKitErrorCode.d.ts +6 -1
- package/dist/esm/index.js +779 -202
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/client/initClient.d.ts +16 -2
- package/dist/esm/types/components/AppWrapper.d.ts +32 -0
- package/dist/esm/types/components/CheckboxButton.d.ts +1 -0
- package/dist/esm/types/components/Dropdown.d.ts +32 -0
- package/dist/esm/types/components/RadioButton.d.ts +2 -0
- package/dist/esm/types/helpers/genRouteHandler.d.ts +8 -0
- package/dist/esm/types/helpers/validators/ChicagoTitleCase.d.ts +9 -0
- package/dist/esm/types/helpers/validators/validURL.d.ts +8 -0
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +24 -0
- package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +33 -0
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/esm/types/types/DropdownItemType.d.ts +6 -0
- package/dist/esm/types/types/ModalType.d.ts +1 -0
- package/dist/esm/types/types/ReactKitErrorCode.d.ts +6 -1
- package/dist/index.d.ts +163 -46
- package/package.json +2 -1
- package/src/client/initClient.tsx +49 -11
- package/src/components/AppWrapper.tsx +258 -2
- package/src/components/CheckboxButton.tsx +24 -6
- package/src/components/Dropdown.tsx +317 -0
- package/src/components/IntelliTable.tsx +1 -1
- package/src/components/ItemPicker/NestableItemList.tsx +1 -0
- package/src/components/LogReviewer.tsx +17 -4
- package/src/components/Modal/index.tsx +4 -1
- package/src/components/MultiSwitch.tsx +1 -1
- package/src/components/RadioButton.tsx +33 -6
- package/src/components/SimpleDateChooser.tsx +8 -2
- package/src/helpers/genRouteHandler.ts +116 -17
- package/src/helpers/logClientEvent.tsx +8 -0
- package/src/helpers/validators/ChicagoTitleCase.test.ts +85 -0
- package/src/helpers/validators/ChicagoTitleCase.ts +32 -0
- package/src/helpers/validators/findURL.test.ts +83 -0
- package/src/helpers/validators/findURL.ts +43 -0
- package/src/helpers/validators/validURL.test.ts +90 -0
- package/src/helpers/validators/validURL.ts +18 -0
- package/src/helpers/visitEndpointOnAnotherServer/index.ts +93 -0
- package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +164 -0
- package/src/helpers/visitServerEndpoint.tsx +1 -0
- package/src/index.ts +8 -0
- package/src/server/initLogCollection.ts +5 -0
- package/src/server/initServer.ts +11 -5
- package/src/types/DropdownItemType.ts +11 -0
- package/src/types/ModalType.tsx +1 -0
- package/src/types/ReactKitErrorCode.tsx +8 -1
|
@@ -240,6 +240,147 @@ export const confirm = async (
|
|
|
240
240
|
});
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
/*----------------------------------------*/
|
|
244
|
+
/* --------------- Prompt -------------- */
|
|
245
|
+
/*----------------------------------------*/
|
|
246
|
+
|
|
247
|
+
// Stored copies of setters
|
|
248
|
+
let setPromptInfo: (
|
|
249
|
+
info: (
|
|
250
|
+
| undefined
|
|
251
|
+
| {
|
|
252
|
+
title: string,
|
|
253
|
+
currentInputFieldText: string,
|
|
254
|
+
opts: {
|
|
255
|
+
textAboveInputField?: string,
|
|
256
|
+
placeholder?: string,
|
|
257
|
+
defaultText?: string,
|
|
258
|
+
confirmButtonText?: string,
|
|
259
|
+
confirmButtonVariant?: Variant,
|
|
260
|
+
cancelButtonText?: string,
|
|
261
|
+
cancelButtonVariant?: Variant,
|
|
262
|
+
minNumChars?: number,
|
|
263
|
+
findValidationError?: (text: string) => string | undefined,
|
|
264
|
+
ariaLabel?: string,
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
) => void;
|
|
269
|
+
|
|
270
|
+
// Function to call when prompt is closed
|
|
271
|
+
let onPromptClosed: (result: string | null) => void;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Show a prompt modal asking the user for input
|
|
275
|
+
* @author Yuen Ler Chow
|
|
276
|
+
* @param title the title text to display at the top of the prompt
|
|
277
|
+
* @param [opts={}] additional options for the prompt dialog
|
|
278
|
+
* @param [opts.textAboveInputField] the text to display in the prompt
|
|
279
|
+
* @param [opts.defaultText] the default text for the input field
|
|
280
|
+
* @param [opts.placeholder] the placeholder text for the input field
|
|
281
|
+
* @param [opts.confirmButtonText=Okay] the text of the confirm button
|
|
282
|
+
* @param [opts.confirmButtonVariant=Variant.Dark] the variant of the confirm button
|
|
283
|
+
* @param [opts.cancelButtonText=Cancel] the text of the cancel button
|
|
284
|
+
* @param [opts.cancelButtonVariant=Variant.Secondary] the variant of the cancel button
|
|
285
|
+
* @param [opts.minNumChars] the minimum number of characters required for
|
|
286
|
+
* the input to be valid
|
|
287
|
+
* @param [opts.findValidationError] a function that takes the input text and
|
|
288
|
+
* returns an error message if the input is invalid, returns undefined if the
|
|
289
|
+
* input is valid
|
|
290
|
+
* @param [opts.ariaLabel] the aria label for the input field
|
|
291
|
+
* @returns Promise that resolves with the input string or null if canceled
|
|
292
|
+
*/
|
|
293
|
+
export const prompt = async (
|
|
294
|
+
title: string,
|
|
295
|
+
opts?: {
|
|
296
|
+
textAboveInputField?: string,
|
|
297
|
+
defaultText?: string,
|
|
298
|
+
placeholder?: string,
|
|
299
|
+
confirmButtonText?: string,
|
|
300
|
+
confirmButtonVariant?: Variant,
|
|
301
|
+
cancelButtonText?: string,
|
|
302
|
+
cancelButtonVariant?: Variant,
|
|
303
|
+
minNumChars?: number,
|
|
304
|
+
findValidationError?: (text: string) => string | undefined,
|
|
305
|
+
ariaLabel?: string,
|
|
306
|
+
},
|
|
307
|
+
): Promise<string | null> => {
|
|
308
|
+
// Wait for helper to exist
|
|
309
|
+
await waitForHelper(() => {
|
|
310
|
+
return !!setPromptInfo;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Fallback if prompt is not available
|
|
314
|
+
if (!setPromptInfo) {
|
|
315
|
+
const resultPassesValidation = false;
|
|
316
|
+
while (!resultPassesValidation) {
|
|
317
|
+
// eslint-disable-next-line no-alert
|
|
318
|
+
const result = window.prompt(
|
|
319
|
+
`${title}\n\n${opts?.textAboveInputField ?? ''}`,
|
|
320
|
+
opts?.defaultText ?? '',
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Exit loop if user cancels
|
|
324
|
+
if (result === null) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Validate min num chars
|
|
329
|
+
const minNumCharsValidationError = (
|
|
330
|
+
(opts?.minNumChars && result.length < opts.minNumChars)
|
|
331
|
+
? `Please enter at least ${opts.minNumChars} characters.`
|
|
332
|
+
: undefined
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Run custom validation
|
|
336
|
+
const customValidationError = (
|
|
337
|
+
opts?.findValidationError
|
|
338
|
+
&& opts.findValidationError(result)
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Show validation issue
|
|
342
|
+
if (minNumCharsValidationError || customValidationError) {
|
|
343
|
+
// Create error message
|
|
344
|
+
const errorMessage = (
|
|
345
|
+
[
|
|
346
|
+
minNumCharsValidationError,
|
|
347
|
+
customValidationError,
|
|
348
|
+
]
|
|
349
|
+
// Filter out undefined messages
|
|
350
|
+
.filter((msg) => {
|
|
351
|
+
return !!msg;
|
|
352
|
+
})
|
|
353
|
+
// Join messages with newlines
|
|
354
|
+
.join('\n')
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Show alert
|
|
358
|
+
alert(
|
|
359
|
+
'Invalid Input',
|
|
360
|
+
errorMessage,
|
|
361
|
+
);
|
|
362
|
+
} else {
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Return promise that resolves with result of prompt
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
// Setup handler
|
|
371
|
+
onPromptClosed = (result: string | null) => {
|
|
372
|
+
resolve(result);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Show the prompt
|
|
376
|
+
setPromptInfo({
|
|
377
|
+
title,
|
|
378
|
+
currentInputFieldText: (opts?.defaultText ?? ''),
|
|
379
|
+
opts: (opts ?? {}),
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
243
384
|
/*----------------------------------------*/
|
|
244
385
|
/* ------------- Fatal Error ------------ */
|
|
245
386
|
/*----------------------------------------*/
|
|
@@ -465,6 +606,28 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
|
|
|
465
606
|
>(undefined);
|
|
466
607
|
setConfirmInfo = setConfirmInfoInner;
|
|
467
608
|
|
|
609
|
+
// Prompt
|
|
610
|
+
const [promptInfo, setPromptInfoInner] = useState<
|
|
611
|
+
| undefined
|
|
612
|
+
| {
|
|
613
|
+
title: string,
|
|
614
|
+
currentInputFieldText: string,
|
|
615
|
+
opts: {
|
|
616
|
+
textAboveInputField?: string,
|
|
617
|
+
placeholder?: string,
|
|
618
|
+
defaultText?: string,
|
|
619
|
+
confirmButtonText?: string,
|
|
620
|
+
confirmButtonVariant?: Variant,
|
|
621
|
+
cancelButtonText?: string,
|
|
622
|
+
cancelButtonVariant?: Variant,
|
|
623
|
+
minNumChars?: number,
|
|
624
|
+
findValidationError?: (text: string) => string | undefined,
|
|
625
|
+
ariaLabel?: string,
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
>(undefined);
|
|
629
|
+
setPromptInfo = setPromptInfoInner;
|
|
630
|
+
|
|
468
631
|
// Session expired
|
|
469
632
|
const [
|
|
470
633
|
sessionHasExpired,
|
|
@@ -531,6 +694,99 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
|
|
|
531
694
|
);
|
|
532
695
|
}
|
|
533
696
|
|
|
697
|
+
/* ------------- Prompt ------------ */
|
|
698
|
+
|
|
699
|
+
if (promptInfo) {
|
|
700
|
+
// Run min char validation
|
|
701
|
+
const minNumCharsValidationError = (
|
|
702
|
+
(
|
|
703
|
+
promptInfo.opts.minNumChars
|
|
704
|
+
&& promptInfo.currentInputFieldText.length < promptInfo.opts.minNumChars
|
|
705
|
+
)
|
|
706
|
+
? `Please enter at least ${promptInfo.opts.minNumChars} characters.`
|
|
707
|
+
: undefined
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Run custom validation
|
|
711
|
+
const customValidationError = (
|
|
712
|
+
promptInfo.opts.findValidationError
|
|
713
|
+
&& promptInfo.opts.findValidationError(promptInfo.currentInputFieldText)
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
modal = (
|
|
717
|
+
<ModalForWrapper
|
|
718
|
+
key={`prompt-${promptInfo.title}`}
|
|
719
|
+
title={promptInfo.title}
|
|
720
|
+
// Don't show ok button if there is a validation error
|
|
721
|
+
type={(
|
|
722
|
+
(customValidationError || minNumCharsValidationError)
|
|
723
|
+
? ModalType.Cancel
|
|
724
|
+
: ModalType.OkayCancel
|
|
725
|
+
)}
|
|
726
|
+
okayLabel={promptInfo.opts.confirmButtonText}
|
|
727
|
+
okayVariant={promptInfo.opts.confirmButtonVariant}
|
|
728
|
+
cancelLabel={promptInfo.opts.cancelButtonText}
|
|
729
|
+
cancelVariant={promptInfo.opts.cancelButtonVariant}
|
|
730
|
+
onClose={(buttonType) => {
|
|
731
|
+
// Get result
|
|
732
|
+
const result = (
|
|
733
|
+
buttonType === ModalButtonType.Okay
|
|
734
|
+
? promptInfo.currentInputFieldText
|
|
735
|
+
: null
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// Close prompt
|
|
739
|
+
setPromptInfo(undefined);
|
|
740
|
+
|
|
741
|
+
// Call handler
|
|
742
|
+
if (onPromptClosed) {
|
|
743
|
+
onPromptClosed(result);
|
|
744
|
+
}
|
|
745
|
+
}}
|
|
746
|
+
onTopOfOtherModals
|
|
747
|
+
dontAllowBackdropExit
|
|
748
|
+
>
|
|
749
|
+
<div>
|
|
750
|
+
{/* Message above input field */}
|
|
751
|
+
{promptInfo.opts.textAboveInputField && (
|
|
752
|
+
<div>
|
|
753
|
+
{promptInfo.opts.textAboveInputField}
|
|
754
|
+
</div>
|
|
755
|
+
)}
|
|
756
|
+
|
|
757
|
+
{/* Input field */}
|
|
758
|
+
<input
|
|
759
|
+
type="text"
|
|
760
|
+
className="form-control"
|
|
761
|
+
aria-label={promptInfo.opts.ariaLabel}
|
|
762
|
+
placeholder={promptInfo.opts.placeholder}
|
|
763
|
+
value={promptInfo.currentInputFieldText}
|
|
764
|
+
onChange={(e) => {
|
|
765
|
+
return setPromptInfo({
|
|
766
|
+
...promptInfo,
|
|
767
|
+
currentInputFieldText: e.target.value,
|
|
768
|
+
});
|
|
769
|
+
}}
|
|
770
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
771
|
+
autoFocus
|
|
772
|
+
/>
|
|
773
|
+
|
|
774
|
+
{/* Validation errors */}
|
|
775
|
+
{minNumCharsValidationError && (
|
|
776
|
+
<div className="text-danger fw-bold mt-2">
|
|
777
|
+
{minNumCharsValidationError}
|
|
778
|
+
</div>
|
|
779
|
+
)}
|
|
780
|
+
{customValidationError && (
|
|
781
|
+
<div className="text-danger fw-bold mt-2">
|
|
782
|
+
{customValidationError}
|
|
783
|
+
</div>
|
|
784
|
+
)}
|
|
785
|
+
</div>
|
|
786
|
+
</ModalForWrapper>
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
534
790
|
/* ------ Custom Modal Portals ------ */
|
|
535
791
|
|
|
536
792
|
// Custom modal portals
|
|
@@ -662,7 +918,7 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
|
|
|
662
918
|
? `
|
|
663
919
|
.tooltip-inner {
|
|
664
920
|
background-color: white;
|
|
665
|
-
color: black;
|
|
921
|
+
color: black !important;
|
|
666
922
|
border: 0.1rem solid black;
|
|
667
923
|
pointer-events: none;
|
|
668
924
|
}
|
|
@@ -675,7 +931,7 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
|
|
|
675
931
|
: `
|
|
676
932
|
.tooltip-inner {
|
|
677
933
|
background-color: black;
|
|
678
|
-
color: white;
|
|
934
|
+
color: white !important;
|
|
679
935
|
border: 0.1rem solid white;
|
|
680
936
|
pointer-events: none;
|
|
681
937
|
}
|
|
@@ -46,6 +46,8 @@ type Props = {
|
|
|
46
46
|
small?: boolean,
|
|
47
47
|
// If true, show a dash in the checkbox (only relevant if unchecked)
|
|
48
48
|
dashed?: boolean,
|
|
49
|
+
// If true, use complex formatting (tabs, newlines) in the text
|
|
50
|
+
useComplexFormatting?: boolean,
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
/*------------------------------------------------------------------------*/
|
|
@@ -80,6 +82,7 @@ const CheckboxButton: React.FC<Props> = (props) => {
|
|
|
80
82
|
),
|
|
81
83
|
small,
|
|
82
84
|
dashed,
|
|
85
|
+
useComplexFormatting,
|
|
83
86
|
} = props;
|
|
84
87
|
|
|
85
88
|
/*------------------------------------------------------------------------*/
|
|
@@ -114,16 +117,31 @@ const CheckboxButton: React.FC<Props> = (props) => {
|
|
|
114
117
|
onChanged(!checked);
|
|
115
118
|
}}
|
|
116
119
|
>
|
|
117
|
-
<div className="d-flex">
|
|
118
|
-
<div className="
|
|
120
|
+
<div className="d-flex align-items-center">
|
|
121
|
+
<div className="me-1">
|
|
119
122
|
<FontAwesomeIcon
|
|
120
123
|
icon={icon}
|
|
121
|
-
className="me-1"
|
|
122
124
|
/>
|
|
123
125
|
</div>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
{
|
|
127
|
+
useComplexFormatting
|
|
128
|
+
? (
|
|
129
|
+
<pre
|
|
130
|
+
className="ps-1 text-start text-break m-0"
|
|
131
|
+
style={{
|
|
132
|
+
whiteSpace: 'pre-wrap',
|
|
133
|
+
tabSize: 2,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{text}
|
|
137
|
+
</pre>
|
|
138
|
+
)
|
|
139
|
+
: (
|
|
140
|
+
<div className="flex-grow-1 text-start text-break">
|
|
141
|
+
{text}
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
127
145
|
</div>
|
|
128
146
|
</button>
|
|
129
147
|
);
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple dropdown menu
|
|
3
|
+
* @author Alessandra De Lucas
|
|
4
|
+
* @author Yuen Ler Chow
|
|
5
|
+
* @author Gabe Abrams
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Import React
|
|
9
|
+
import React, { useEffect, useReducer, useRef } from 'react';
|
|
10
|
+
|
|
11
|
+
// Import helpers
|
|
12
|
+
import combineClassNames from '../helpers/combineClassNames';
|
|
13
|
+
|
|
14
|
+
// Import shared types
|
|
15
|
+
import Variant from '../types/Variant';
|
|
16
|
+
import DropdownItemType from '../types/DropdownItemType';
|
|
17
|
+
|
|
18
|
+
// Check dark mode
|
|
19
|
+
import { isDarkModeOn } from '../client/initClient';
|
|
20
|
+
|
|
21
|
+
/*------------------------------------------------------------------------*/
|
|
22
|
+
/* -------------------------------- Types ------------------------------- */
|
|
23
|
+
/*------------------------------------------------------------------------*/
|
|
24
|
+
|
|
25
|
+
// Props definition
|
|
26
|
+
type Props = {
|
|
27
|
+
// List of items in the dropdown menu
|
|
28
|
+
items: DropdownItem[],
|
|
29
|
+
// Information about the dropdown button
|
|
30
|
+
dropdownButton: {
|
|
31
|
+
// Aria label for accessibility
|
|
32
|
+
ariaLabel: string,
|
|
33
|
+
// Unique ID
|
|
34
|
+
id: string,
|
|
35
|
+
// Button content
|
|
36
|
+
content?: React.ReactNode,
|
|
37
|
+
// Variant
|
|
38
|
+
variant?: Variant,
|
|
39
|
+
},
|
|
40
|
+
// If true, no arrow shows in dropdown
|
|
41
|
+
// noArrow?: boolean,
|
|
42
|
+
// Size of dropdown
|
|
43
|
+
// size?: DropdownSize,
|
|
44
|
+
// Direction of dropdown
|
|
45
|
+
// direction?: DropdownDirection,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Details about a dropdown item
|
|
49
|
+
type DropdownItem = (
|
|
50
|
+
| {
|
|
51
|
+
// Header element
|
|
52
|
+
type: DropdownItemType.Header,
|
|
53
|
+
// Header content
|
|
54
|
+
content: React.ReactNode,
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
// Divider element
|
|
58
|
+
type: DropdownItemType.Divider,
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
// Item from dropdown menu
|
|
62
|
+
type: DropdownItemType.Item,
|
|
63
|
+
// Item content
|
|
64
|
+
content: React.ReactNode,
|
|
65
|
+
// Item aria label
|
|
66
|
+
ariaLabel: string,
|
|
67
|
+
// Unique item ID
|
|
68
|
+
id: string,
|
|
69
|
+
// Click function for dropdown item
|
|
70
|
+
onClick: () => void,
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Dropdown menu sizes
|
|
75
|
+
// enum DropdownSize {
|
|
76
|
+
// // Small dropdown size
|
|
77
|
+
// Small = 'Small',
|
|
78
|
+
// // Medium dropdown size
|
|
79
|
+
// Medium = 'Medium',
|
|
80
|
+
// // Large dropdown size
|
|
81
|
+
// Large = 'Large',
|
|
82
|
+
// }
|
|
83
|
+
|
|
84
|
+
// Dropdown menu direction
|
|
85
|
+
// enum DropdownDirection {
|
|
86
|
+
// // Dropdown menu appears centered
|
|
87
|
+
// Centered = 'Centered',
|
|
88
|
+
// // Dropdown menu appears to right of button
|
|
89
|
+
// Right = 'Right',
|
|
90
|
+
// // Dropdown menu appears to left of button
|
|
91
|
+
// Left = 'Left',
|
|
92
|
+
// // Dropdown menu appears above the button
|
|
93
|
+
// Up = 'Up',
|
|
94
|
+
// // Dropdown menu appears below the button
|
|
95
|
+
// Down = 'Down',
|
|
96
|
+
// }
|
|
97
|
+
|
|
98
|
+
/*------------------------------------------------------------------------*/
|
|
99
|
+
/* -------------------------------- State ------------------------------- */
|
|
100
|
+
/*------------------------------------------------------------------------*/
|
|
101
|
+
|
|
102
|
+
/* -------- State Definition -------- */
|
|
103
|
+
|
|
104
|
+
type State = {
|
|
105
|
+
// If true, the dropdown menu is open
|
|
106
|
+
isDropdownOpen: boolean,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/* ------------- Actions ------------ */
|
|
110
|
+
|
|
111
|
+
// Types of actions
|
|
112
|
+
enum ActionType {
|
|
113
|
+
// Toggle opening the dropdown menu
|
|
114
|
+
ToggleDropdown = 'ToggleDropdown',
|
|
115
|
+
// Close the dropdown menu
|
|
116
|
+
CloseDropdown = 'CloseDropdown',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Action definitions
|
|
120
|
+
type Action = (
|
|
121
|
+
| {
|
|
122
|
+
type: ActionType.ToggleDropdown,
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
type: ActionType.CloseDropdown,
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reducer that executes actions
|
|
131
|
+
* @author Alessandra De Lucas
|
|
132
|
+
* @author Yuen Ler Chow
|
|
133
|
+
* @param state current state
|
|
134
|
+
* @param action action to execute
|
|
135
|
+
*/
|
|
136
|
+
const reducer = (state: State, action: Action): State => {
|
|
137
|
+
switch (action.type) {
|
|
138
|
+
case ActionType.ToggleDropdown: {
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
isDropdownOpen: !state.isDropdownOpen,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
case ActionType.CloseDropdown: {
|
|
145
|
+
return {
|
|
146
|
+
...state,
|
|
147
|
+
isDropdownOpen: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
default: {
|
|
151
|
+
return state;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/*------------------------------------------------------------------------*/
|
|
157
|
+
/* ------------------------------ Component ----------------------------- */
|
|
158
|
+
/*------------------------------------------------------------------------*/
|
|
159
|
+
|
|
160
|
+
const Dropdown: React.FC<Props> = (props) => {
|
|
161
|
+
/*------------------------------------------------------------------------*/
|
|
162
|
+
/* -------------------------------- Setup ------------------------------- */
|
|
163
|
+
/*------------------------------------------------------------------------*/
|
|
164
|
+
|
|
165
|
+
/* -------------- Props ------------- */
|
|
166
|
+
|
|
167
|
+
// Destructure all props
|
|
168
|
+
const {
|
|
169
|
+
dropdownButton,
|
|
170
|
+
items,
|
|
171
|
+
} = props;
|
|
172
|
+
|
|
173
|
+
/* -------------- State ------------- */
|
|
174
|
+
|
|
175
|
+
// Initial state
|
|
176
|
+
const initialState: State = {
|
|
177
|
+
isDropdownOpen: false,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Initialize state
|
|
181
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
182
|
+
|
|
183
|
+
// Destructure common state
|
|
184
|
+
const {
|
|
185
|
+
isDropdownOpen,
|
|
186
|
+
} = state;
|
|
187
|
+
|
|
188
|
+
/* -------------- Refs -------------- */
|
|
189
|
+
|
|
190
|
+
// Initialize refs
|
|
191
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
192
|
+
|
|
193
|
+
/*------------------------------------------------------------------------*/
|
|
194
|
+
/* ------------------------- Component Functions ------------------------ */
|
|
195
|
+
/*------------------------------------------------------------------------*/
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle clicking outside of the dropdown menu to close it
|
|
199
|
+
* @author Yuen Ler Chow
|
|
200
|
+
* @param event the mouse event
|
|
201
|
+
*/
|
|
202
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
203
|
+
if (
|
|
204
|
+
// Dropdown has been rendered
|
|
205
|
+
dropdownRef.current
|
|
206
|
+
// Click occurred outside the dropdown
|
|
207
|
+
&& !dropdownRef.current.contains(event.target as Element)
|
|
208
|
+
) {
|
|
209
|
+
dispatch({ type: ActionType.CloseDropdown });
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/*------------------------------------------------------------------------*/
|
|
214
|
+
/* ------------------------- Lifecycle Functions ------------------------ */
|
|
215
|
+
/*------------------------------------------------------------------------*/
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Mount
|
|
219
|
+
* @author Yuen Ler Chow
|
|
220
|
+
*/
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
// Add event listener to close dropdown when clicking outside
|
|
223
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
224
|
+
|
|
225
|
+
// Cleanup
|
|
226
|
+
return () => {
|
|
227
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
228
|
+
};
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
/*----------------------------------------*/
|
|
232
|
+
/* --------------- Main UI -------------- */
|
|
233
|
+
/*----------------------------------------*/
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div
|
|
237
|
+
className="dropdown"
|
|
238
|
+
ref={dropdownRef}
|
|
239
|
+
data-bs-theme={isDarkModeOn() ? 'dark' : undefined}
|
|
240
|
+
>
|
|
241
|
+
<button
|
|
242
|
+
className={
|
|
243
|
+
combineClassNames([
|
|
244
|
+
'btn dropdown-toggle border',
|
|
245
|
+
isDropdownOpen && 'show',
|
|
246
|
+
`btn-${dropdownButton.variant}`,
|
|
247
|
+
dropdownButton.variant === Variant.Light && 'text-dark',
|
|
248
|
+
])
|
|
249
|
+
}
|
|
250
|
+
type="button"
|
|
251
|
+
id={dropdownButton.id}
|
|
252
|
+
aria-expanded={isDropdownOpen}
|
|
253
|
+
aria-label={dropdownButton.ariaLabel}
|
|
254
|
+
onClick={() => {
|
|
255
|
+
dispatch({
|
|
256
|
+
type: ActionType.ToggleDropdown,
|
|
257
|
+
});
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
{
|
|
261
|
+
dropdownButton.content
|
|
262
|
+
}
|
|
263
|
+
</button>
|
|
264
|
+
<ul
|
|
265
|
+
className={
|
|
266
|
+
combineClassNames([
|
|
267
|
+
'dropdown-menu',
|
|
268
|
+
isDarkModeOn() && 'dropdown-menu-dark',
|
|
269
|
+
isDropdownOpen && 'show',
|
|
270
|
+
])
|
|
271
|
+
}
|
|
272
|
+
>
|
|
273
|
+
{Object.values(items).map((item) => {
|
|
274
|
+
if (item.type === DropdownItemType.Header) {
|
|
275
|
+
return (
|
|
276
|
+
// TODO: Implement header
|
|
277
|
+
<span />
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (item.type === DropdownItemType.Divider) {
|
|
281
|
+
return (
|
|
282
|
+
// TODO: Implement divider
|
|
283
|
+
<span />
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return (
|
|
287
|
+
<li
|
|
288
|
+
key={item.id}
|
|
289
|
+
>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
aria-label={item.ariaLabel}
|
|
293
|
+
className="dropdown-item"
|
|
294
|
+
onClick={(e) => {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
dispatch({
|
|
297
|
+
type: ActionType.CloseDropdown,
|
|
298
|
+
});
|
|
299
|
+
item.onClick();
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
{item.content}
|
|
303
|
+
</button>
|
|
304
|
+
</li>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
307
|
+
</ul>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/*------------------------------------------------------------------------*/
|
|
313
|
+
/* ------------------------------- Wrap Up ------------------------------ */
|
|
314
|
+
/*------------------------------------------------------------------------*/
|
|
315
|
+
|
|
316
|
+
// Export component
|
|
317
|
+
export default Dropdown;
|
|
@@ -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.
|
|
318
|
+
uncheckedVariant={Variant.Light}
|
|
319
319
|
/>
|
|
320
320
|
);
|
|
321
321
|
})
|
|
@@ -959,10 +959,23 @@ const LogReviewer: React.FC<Props> = (props) => {
|
|
|
959
959
|
const { year, month } = toLoad[i];
|
|
960
960
|
|
|
961
961
|
// Load
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
962
|
+
let logs: Log[] = [];
|
|
963
|
+
let pageNumber = 1;
|
|
964
|
+
let hasAnotherPage = true;
|
|
965
|
+
|
|
966
|
+
while (hasAnotherPage) {
|
|
967
|
+
const response = await visitServerEndpoint({
|
|
968
|
+
path: `${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/${year}/months/${month}`,
|
|
969
|
+
method: 'GET',
|
|
970
|
+
params: {
|
|
971
|
+
pageNumber,
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
logs = logs.concat(response.items);
|
|
976
|
+
hasAnotherPage = response.hasAnotherPage;
|
|
977
|
+
pageNumber += 1;
|
|
978
|
+
}
|
|
966
979
|
|
|
967
980
|
// Add to map
|
|
968
981
|
if (!logMap[year]) {
|