create-hhmi-example 1.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.
- package/README.md +78 -0
- package/copy-template.js +76 -0
- package/index.js +254 -0
- package/package.json +17 -0
- package/template/hhmiExample.Server/Program.cs +167 -0
- package/template/hhmiExample.Server/Properties/launchSettings.json +44 -0
- package/template/hhmiExample.Server/appsettings.Development.json +8 -0
- package/template/hhmiExample.Server/appsettings.json +9 -0
- package/template/hhmiExample.Server/hhmiExample.Server.csproj +50 -0
- package/template/hhmiExample.Server/hhmiExample.Server.http +6 -0
- package/template/hhmiExample.sln +33 -0
- package/template/hhmiexample.client/eslint.config.js +23 -0
- package/template/hhmiexample.client/hhmiexample.client.esproj +12 -0
- package/template/hhmiexample.client/index.html +13 -0
- package/template/hhmiexample.client/package-lock.json +6490 -0
- package/template/hhmiexample.client/package.json +42 -0
- package/template/hhmiexample.client/prompts/README.md +12 -0
- package/template/hhmiexample.client/prompts/REQUIREMENTS.md +113 -0
- package/template/hhmiexample.client/public/favicon.ico +0 -0
- package/template/hhmiexample.client/public/vite.svg +1 -0
- package/template/hhmiexample.client/src/App.css +11 -0
- package/template/hhmiexample.client/src/App.tsx +147 -0
- package/template/hhmiexample.client/src/assets/logo-black.png +0 -0
- package/template/hhmiexample.client/src/assets/logo-white.png +0 -0
- package/template/hhmiexample.client/src/assets/react.svg +1 -0
- package/template/hhmiexample.client/src/components/AppFrame/AppFrame.tsx +796 -0
- package/template/hhmiexample.client/src/components/AppFrame/Theme.tsx +98 -0
- package/template/hhmiexample.client/src/components/AppFrame/UserSettingPage.tsx +91 -0
- package/template/hhmiexample.client/src/components/AppFrame/UserSettings.tsx +146 -0
- package/template/hhmiexample.client/src/components/AppFrame/modules/ExampleConfig.tsx +86 -0
- package/template/hhmiexample.client/src/components/AppFrame/modules/index.ts +8 -0
- package/template/hhmiexample.client/src/components/AppFrame/types.ts +48 -0
- package/template/hhmiexample.client/src/components/Global/HHMIControls.tsx +567 -0
- package/template/hhmiexample.client/src/components/Global/Quill.tsx +60 -0
- package/template/hhmiexample.client/src/index.css +11 -0
- package/template/hhmiexample.client/src/main.tsx +17 -0
- package/template/hhmiexample.client/src/pages/Example/ExampleConfigurationPage.tsx +24 -0
- package/template/hhmiexample.client/src/pages/Example/ExampleHomePage.tsx +23 -0
- package/template/hhmiexample.client/src/pages/LandingPage.tsx +36 -0
- package/template/hhmiexample.client/src/pages/NotAuthorizedPage.tsx +18 -0
- package/template/hhmiexample.client/src/services/AppService.ts +297 -0
- package/template/hhmiexample.client/src/types/IExampleUser.ts +19 -0
- package/template/hhmiexample.client/src/types/IMessageLocation.ts +8 -0
- package/template/hhmiexample.client/src/vite-env.d.ts +4 -0
- package/template/hhmiexample.client/tsconfig.app.json +27 -0
- package/template/hhmiexample.client/tsconfig.json +11 -0
- package/template/hhmiexample.client/tsconfig.node.json +25 -0
- package/template/hhmiexample.client/vite.config.ts +61 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
2
|
+
import React, { type ChangeEvent, type ReactElement, type ReactNode } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogBody,
|
|
7
|
+
DialogContent,
|
|
8
|
+
type DialogOpenChangeEvent,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
MessageBar,
|
|
11
|
+
DataGrid,
|
|
12
|
+
DataGridBody,
|
|
13
|
+
DataGridRow,
|
|
14
|
+
DataGridHeader,
|
|
15
|
+
DataGridHeaderCell,
|
|
16
|
+
DataGridCell,
|
|
17
|
+
Popover,
|
|
18
|
+
PopoverTrigger,
|
|
19
|
+
PopoverSurface,
|
|
20
|
+
type PopoverProps,
|
|
21
|
+
OverlayDrawer,
|
|
22
|
+
DrawerHeader,
|
|
23
|
+
DrawerHeaderTitle,
|
|
24
|
+
DrawerBody,
|
|
25
|
+
DrawerFooter,
|
|
26
|
+
Label,
|
|
27
|
+
Checkbox,
|
|
28
|
+
type CheckboxOnChangeData,
|
|
29
|
+
DialogActions,
|
|
30
|
+
DialogSurface,
|
|
31
|
+
Field,
|
|
32
|
+
makeStyles,
|
|
33
|
+
Input,
|
|
34
|
+
type GriffelStyle,
|
|
35
|
+
Textarea,
|
|
36
|
+
MessageBarGroup,
|
|
37
|
+
type ComboboxProps,
|
|
38
|
+
Combobox,
|
|
39
|
+
Persona,
|
|
40
|
+
Accordion,
|
|
41
|
+
AccordionItem,
|
|
42
|
+
AccordionHeader,
|
|
43
|
+
AccordionPanel,
|
|
44
|
+
tokens,
|
|
45
|
+
shorthands,
|
|
46
|
+
type FieldProps,
|
|
47
|
+
type TextareaProps,
|
|
48
|
+
type InputProps,
|
|
49
|
+
type DataGridProps,
|
|
50
|
+
type ButtonProps,
|
|
51
|
+
type LabelProps,
|
|
52
|
+
type MessageBarGroupProps,
|
|
53
|
+
type MessageBarProps,
|
|
54
|
+
Dropdown,
|
|
55
|
+
Option,
|
|
56
|
+
Spinner,
|
|
57
|
+
Switch,
|
|
58
|
+
type SwitchOnChangeData,
|
|
59
|
+
type SelectionEvents,
|
|
60
|
+
type OptionOnSelectData,
|
|
61
|
+
type OpenPopoverEvents,
|
|
62
|
+
type OnOpenChangeData,
|
|
63
|
+
useId
|
|
64
|
+
} from '@fluentui/react-components';
|
|
65
|
+
import { Dismiss24Regular } from '@fluentui/react-icons'
|
|
66
|
+
import { searchUserByEmailWDHCM, type IUserQueryResult } from '../../services/AppService';
|
|
67
|
+
import { DataGridBody as DataGridBodyVirtualized } from '@fluentui-contrib/react-data-grid-react-window';
|
|
68
|
+
|
|
69
|
+
export interface IHHMIHomeIcon {
|
|
70
|
+
key: string;
|
|
71
|
+
text: string;
|
|
72
|
+
icon: ReactNode;
|
|
73
|
+
onClick: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const MessageBarTypeOptions = {
|
|
77
|
+
info: 'info',
|
|
78
|
+
success: 'success',
|
|
79
|
+
warning: 'warning',
|
|
80
|
+
error: 'error'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const HHMIPeoplePickerLocations = {
|
|
84
|
+
Janelia: 'janelia.hhmi.org'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function _removeDuplicates(personas: IUserQueryResult[], possibleDupes: IUserQueryResult[]) {
|
|
88
|
+
return personas.filter(persona => !_listContainsPersona(persona, possibleDupes));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _listContainsPersona(persona: IUserQueryResult, personas: IUserQueryResult[]) {
|
|
92
|
+
if (!personas || !personas.length || personas.length === 0) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return personas.filter(item => {
|
|
96
|
+
if (item.id && persona.id) {
|
|
97
|
+
return item.id.toLowerCase() === persona.id.toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
return item.displayName === persona.displayName;
|
|
100
|
+
}).length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface IHHMIPeoplePickerProps extends ComboboxProps {
|
|
104
|
+
selectedUsers: IUserQueryResult[];
|
|
105
|
+
onSelectUser: (selectedOptions: IUserQueryResult[]) => void;
|
|
106
|
+
location?: string | undefined;
|
|
107
|
+
ariaLabel?: string | undefined;
|
|
108
|
+
ariaLabelledBy?: string | undefined;
|
|
109
|
+
containerClassName?: string | undefined;
|
|
110
|
+
clearValueAfterSelect?: boolean;
|
|
111
|
+
customValue?: string;
|
|
112
|
+
customOnChange?: (value: string) => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function HHMIPeoplePicker(props: IHHMIPeoplePickerProps) {
|
|
116
|
+
const [matchingOptions, setMatchingOptions] = React.useState<IUserQueryResult[]>([]);
|
|
117
|
+
const [peoplePickerValue, setPeoplePickerValue] = React.useState<string>('');
|
|
118
|
+
|
|
119
|
+
const selectedOptions: string[] =
|
|
120
|
+
(props.selectedUsers && props.selectedUsers.length > 0)
|
|
121
|
+
? [...props.selectedUsers.map((option: IUserQueryResult) => { return option.id; })]
|
|
122
|
+
: [];
|
|
123
|
+
|
|
124
|
+
const onSearchPeoplePicker: ComboboxProps["onChange"] = (event) => {
|
|
125
|
+
setPeoplePickerValue(event.target.value);
|
|
126
|
+
if (props.customOnChange) {
|
|
127
|
+
props.customOnChange(event.target.value);
|
|
128
|
+
}
|
|
129
|
+
const value = event.target.value.trim();
|
|
130
|
+
if (value.length > 2) {
|
|
131
|
+
let matches: IUserQueryResult[] = [];
|
|
132
|
+
searchUserByEmailWDHCM(value).then((searchResults: IUserQueryResult[]) => {
|
|
133
|
+
if (searchResults) {
|
|
134
|
+
matches = _removeDuplicates(searchResults, props.selectedUsers ? props.selectedUsers : []);
|
|
135
|
+
if (props.location !== null && props.location !== undefined) {
|
|
136
|
+
const location = props.location !== undefined ? props.location : '';
|
|
137
|
+
matches = matches.filter((user: IUserQueryResult) => {
|
|
138
|
+
if (user.mail && user.mail.indexOf(location) > -1) return true;
|
|
139
|
+
if (user.id && user.id.indexOf(location) > -1) return true;
|
|
140
|
+
if (user.officeLocation && user.officeLocation.indexOf(location) > -1) return true;
|
|
141
|
+
return false;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
setMatchingOptions(matches);
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
setMatchingOptions([]);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className={props.containerClassName}>
|
|
154
|
+
<Combobox
|
|
155
|
+
{...props}
|
|
156
|
+
aria-label={props.ariaLabel}
|
|
157
|
+
aria-labelledby={props.ariaLabelledBy}
|
|
158
|
+
freeform={true}
|
|
159
|
+
value={props.customValue ?? peoplePickerValue}
|
|
160
|
+
onChange={onSearchPeoplePicker}
|
|
161
|
+
selectedOptions={selectedOptions}
|
|
162
|
+
onOptionSelect={(_e, selectedData) => {
|
|
163
|
+
if (selectedData && selectedData.optionValue && selectedData.optionText) {
|
|
164
|
+
const selectedOptions: IUserQueryResult[] = matchingOptions ? matchingOptions.filter((option: IUserQueryResult) => { return option.id === selectedData.optionValue }) : [];
|
|
165
|
+
props.onSelectUser(selectedOptions);
|
|
166
|
+
if (props.clearValueAfterSelect) {
|
|
167
|
+
setPeoplePickerValue('');
|
|
168
|
+
} else {
|
|
169
|
+
setPeoplePickerValue(selectedData.optionText);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{matchingOptions.length > 0 && matchingOptions.map((option: IUserQueryResult) => (
|
|
175
|
+
<Option key={option.id} value={option.id} text={option.displayName}>
|
|
176
|
+
<Persona
|
|
177
|
+
size={'small'}
|
|
178
|
+
avatar={{ color: "colorful", image: { src: option.photoURL } }}
|
|
179
|
+
name={option.displayName}
|
|
180
|
+
secondaryText={null}
|
|
181
|
+
textAlignment={'center'}
|
|
182
|
+
/>
|
|
183
|
+
</Option>
|
|
184
|
+
))}
|
|
185
|
+
{matchingOptions.length === 0 && <Option key="no-results" text="">No results found</Option>}
|
|
186
|
+
</Combobox>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface INavLinkStyles {
|
|
192
|
+
accordionHeader: GriffelStyle;
|
|
193
|
+
accordionPanel: GriffelStyle;
|
|
194
|
+
accordionButton: GriffelStyle;
|
|
195
|
+
iconDiv: GriffelStyle;
|
|
196
|
+
accordionButtonSelected: GriffelStyle;
|
|
197
|
+
accordionButtonText: GriffelStyle;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface INavLinkKeyUrl { key: string; url: string; }
|
|
201
|
+
|
|
202
|
+
export interface INavLink {
|
|
203
|
+
key: string;
|
|
204
|
+
name: string;
|
|
205
|
+
url: string;
|
|
206
|
+
icon: ReactElement;
|
|
207
|
+
onClick: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface INavLinkGroup {
|
|
211
|
+
name: string;
|
|
212
|
+
collapseByDefault: boolean;
|
|
213
|
+
expandAriaLabel: string;
|
|
214
|
+
collapseAriaLabel: string;
|
|
215
|
+
links: INavLink[];
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export interface IHHMILeftNavProps {
|
|
219
|
+
linkGroups: INavLinkGroup[];
|
|
220
|
+
onLinkClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, link: INavLink) => void;
|
|
221
|
+
styles: INavLinkStyles;
|
|
222
|
+
selectedLink: string;
|
|
223
|
+
defaultOpenSection?: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function HHMILeftNav(props: IHHMILeftNavProps) {
|
|
227
|
+
const useStyles = makeStyles({ ...props.styles });
|
|
228
|
+
const styles = useStyles();
|
|
229
|
+
return (
|
|
230
|
+
<Accordion multiple={true} collapsible={true} defaultOpenItems={props.defaultOpenSection}>
|
|
231
|
+
{props.linkGroups.map((group: INavLinkGroup, i: number) => (
|
|
232
|
+
<AccordionItem key={i} value={i.toString()} style={{ marginTop: tokens.spacingVerticalSNudge }}>
|
|
233
|
+
<AccordionHeader className={styles.accordionHeader}>{group.name as string}</AccordionHeader>
|
|
234
|
+
{group.links.map((link: INavLink) => (
|
|
235
|
+
<AccordionPanel key={link.key} className={styles.accordionPanel}>
|
|
236
|
+
<button className={`${styles.accordionButton} ` + (props.selectedLink === link.key ? styles.accordionButtonSelected : '')} onClick={(e) => { props.onLinkClick(e, link); }}>
|
|
237
|
+
<div className={styles.iconDiv}>{link.icon}</div>
|
|
238
|
+
<div className={styles.accordionButtonText}>{link.name}</div>
|
|
239
|
+
</button>
|
|
240
|
+
</AccordionPanel>
|
|
241
|
+
))}
|
|
242
|
+
</AccordionItem>
|
|
243
|
+
))}
|
|
244
|
+
</Accordion>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
type IHHMIButtonProps = ButtonProps & { ariaLabel?: string | undefined; text?: string | undefined; }
|
|
249
|
+
|
|
250
|
+
export function HHMIButtonDefault(props: IHHMIButtonProps) {
|
|
251
|
+
return <Button {...props}>{props.text}</Button>;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function HHMIButtonPrimary(props: IHHMIButtonProps) {
|
|
255
|
+
return <Button {...props} appearance={'primary'}>{props.text}</Button>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface IHHMICheckBoxProps {
|
|
259
|
+
ariaLabel?: string;
|
|
260
|
+
checked?: boolean;
|
|
261
|
+
className?: string;
|
|
262
|
+
disabled?: boolean;
|
|
263
|
+
label?: string;
|
|
264
|
+
onChange?: ((ev: ChangeEvent<HTMLInputElement>, data: CheckboxOnChangeData) => void);
|
|
265
|
+
required?: boolean;
|
|
266
|
+
style?: React.CSSProperties;
|
|
267
|
+
size?: "medium" | "large";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function HHMICheckBox(props: IHHMICheckBoxProps) {
|
|
271
|
+
return <Checkbox {...props} />;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface IHHMIDropDownProps {
|
|
275
|
+
onOptionSelect: (event: SelectionEvents, data: OptionOnSelectData) => void;
|
|
276
|
+
multiselect: boolean;
|
|
277
|
+
selectedOptions: string[];
|
|
278
|
+
options: React.ReactElement[];
|
|
279
|
+
value?: string;
|
|
280
|
+
placeholder?: string;
|
|
281
|
+
style?: React.CSSProperties;
|
|
282
|
+
disabled?: boolean;
|
|
283
|
+
className?: string | undefined;
|
|
284
|
+
display?: string;
|
|
285
|
+
id?: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function HHMIDropDown(props: IHHMIDropDownProps) {
|
|
289
|
+
return (
|
|
290
|
+
<div style={{ display: props.display ? props.display : 'block' }}>
|
|
291
|
+
<Dropdown {...props} disabled={props.disabled ? true : false} placeholder={props.placeholder ? props.placeholder : ''} style={props.style} onOptionSelect={props.onOptionSelect} multiselect={props.multiselect} selectedOptions={props.selectedOptions} id={props.id ? props.id : undefined}>{props.options}</Dropdown>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface IHHMILabelProps extends LabelProps { children: React.ReactNode | string; }
|
|
297
|
+
|
|
298
|
+
export function HHMILabel(props: IHHMILabelProps) {
|
|
299
|
+
return <Label {...props}>{props.children}</Label>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface IHHMIPopoverProps extends PopoverProps {
|
|
303
|
+
content: React.ReactNode;
|
|
304
|
+
children: React.ReactElement;
|
|
305
|
+
setIsOpen: (isOpen: boolean) => void;
|
|
306
|
+
style?: React.CSSProperties;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function HHMIPopover(props: IHHMIPopoverProps) {
|
|
310
|
+
return (
|
|
311
|
+
<Popover {...props} onOpenChange={(_: OpenPopoverEvents, data: OnOpenChangeData) => { props.setIsOpen(data.open); }}>
|
|
312
|
+
<PopoverTrigger disableButtonEnhancement>{props.children}</PopoverTrigger>
|
|
313
|
+
<PopoverSurface>{props.content}</PopoverSurface>
|
|
314
|
+
</Popover>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface IHHMIMessageBarProps {
|
|
319
|
+
isVisible: boolean;
|
|
320
|
+
group?: MessageBarGroupProps;
|
|
321
|
+
messageBar: MessageBarProps;
|
|
322
|
+
children: React.ReactNode | string;
|
|
323
|
+
style?: React.CSSProperties;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function HHMIMessageBar(props: IHHMIMessageBarProps) {
|
|
327
|
+
return (
|
|
328
|
+
<MessageBarGroup animate={props.group?.animate ? props.group?.animate : 'both'}>
|
|
329
|
+
{!props.isVisible ? <></> : <MessageBar {...props.messageBar}>{props.children}</MessageBar>}
|
|
330
|
+
</MessageBarGroup>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface IHHMITextFieldProps {
|
|
335
|
+
fieldProps?: Partial<FieldProps>;
|
|
336
|
+
textAreaProps?: Partial<TextareaProps>;
|
|
337
|
+
inputProps?: Partial<InputProps>;
|
|
338
|
+
ariaLabel?: string;
|
|
339
|
+
multiline?: boolean;
|
|
340
|
+
styles?: GriffelStyle;
|
|
341
|
+
contentAfter?: React.ReactNode;
|
|
342
|
+
className?: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function HHMITextField(props: IHHMITextFieldProps) {
|
|
346
|
+
const useStyles = makeStyles({ textField: { ...props.styles } });
|
|
347
|
+
const HHMITextFieldStyles = useStyles().textField;
|
|
348
|
+
const contentAfter = <>{props.contentAfter ? props.contentAfter : ''}</>;
|
|
349
|
+
return (
|
|
350
|
+
<Field {...props.fieldProps} className={props.className + ' ' + HHMITextFieldStyles} aria-label={props.ariaLabel}>
|
|
351
|
+
{props.multiline && <Textarea {...props.textAreaProps} />}
|
|
352
|
+
{!props.multiline && <Input {...props.inputProps} contentAfter={contentAfter} />}
|
|
353
|
+
</Field>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
interface IHHMIToggleProps {
|
|
358
|
+
label?: string;
|
|
359
|
+
labelPosition?: "before" | "after" | "above";
|
|
360
|
+
checked?: boolean;
|
|
361
|
+
className?: string;
|
|
362
|
+
disabled?: boolean;
|
|
363
|
+
id?: string;
|
|
364
|
+
onChange?: ((event: ChangeEvent<HTMLInputElement>, data: SwitchOnChangeData) => void);
|
|
365
|
+
onText?: string;
|
|
366
|
+
offText?: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function HHMIToggle(props: IHHMIToggleProps) {
|
|
370
|
+
return <Switch {...props} />;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export const defaultDateAnchor = new Date('January 1, 2000 00:00:00');
|
|
374
|
+
|
|
375
|
+
interface IHHMIDialogProps {
|
|
376
|
+
dialogTitle?: string | JSX.Element;
|
|
377
|
+
dialogContent: React.ReactNode | string;
|
|
378
|
+
dialogActions: React.ReactNode | string;
|
|
379
|
+
hidden?: boolean;
|
|
380
|
+
onDismiss?: ((ev?: DialogOpenChangeEvent | undefined) => void);
|
|
381
|
+
type?: 'alert' | 'modal' | 'non-modal';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function HHMIDialog(props: IHHMIDialogProps) {
|
|
385
|
+
return (
|
|
386
|
+
<Dialog modalType={props.type ? props.type : 'alert'} open={props.hidden ? !props.hidden : true} defaultOpen={false} onOpenChange={props.onDismiss ? props.onDismiss : () => { }}>
|
|
387
|
+
<DialogSurface>
|
|
388
|
+
<DialogBody>
|
|
389
|
+
<DialogTitle>{props.dialogTitle}</DialogTitle>
|
|
390
|
+
<DialogContent>{props.dialogContent}</DialogContent>
|
|
391
|
+
<DialogActions>{props.dialogActions}</DialogActions>
|
|
392
|
+
</DialogBody>
|
|
393
|
+
</DialogSurface>
|
|
394
|
+
</Dialog>
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface IHHMIDrawerProps {
|
|
399
|
+
title: string;
|
|
400
|
+
content: React.ReactNode;
|
|
401
|
+
footer: React.ReactNode;
|
|
402
|
+
open: boolean;
|
|
403
|
+
onOpenChange: () => void;
|
|
404
|
+
closeDrawer: () => void;
|
|
405
|
+
position: 'start' | 'end';
|
|
406
|
+
size?: "small" | "medium" | "large" | "full";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function HHMIDrawer(props: IHHMIDrawerProps) {
|
|
410
|
+
return (
|
|
411
|
+
<div>
|
|
412
|
+
<OverlayDrawer open={props.open} onOpenChange={props.onOpenChange} position={props.position} size={props.size ? props.size : 'small'}>
|
|
413
|
+
<DrawerHeader>
|
|
414
|
+
<DrawerHeaderTitle action={<Button appearance="subtle" aria-label="Close" icon={<Dismiss24Regular />} onClick={props.closeDrawer} />}>{props.title}</DrawerHeaderTitle>
|
|
415
|
+
</DrawerHeader>
|
|
416
|
+
<DrawerBody>{props.content}</DrawerBody>
|
|
417
|
+
<DrawerFooter>{props.footer}</DrawerFooter>
|
|
418
|
+
</OverlayDrawer>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
type IHHMIDataGridProps = DataGridProps & { dataLoaded: boolean; }
|
|
424
|
+
|
|
425
|
+
export function HHMIDataGrid(props: IHHMIDataGridProps) {
|
|
426
|
+
const dataGrid = makeStyles({ dataGridRowSelected: { color: tokens.colorBrandBackgroundInverted } });
|
|
427
|
+
const dataGridRowStyles = dataGrid().dataGridRowSelected;
|
|
428
|
+
const selectedItemId: number | undefined = props.selectedItems ? Array.from(props.selectedItems)[0] as number : undefined;
|
|
429
|
+
return (
|
|
430
|
+
<div style={{ maxWidth: '99%' }}>
|
|
431
|
+
<DataGrid {...props}>
|
|
432
|
+
<DataGridHeader>
|
|
433
|
+
<DataGridRow selectionCell={{ "aria-label": "Select all rows" }}>
|
|
434
|
+
{({ renderHeaderCell }) => <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>}
|
|
435
|
+
</DataGridRow>
|
|
436
|
+
</DataGridHeader>
|
|
437
|
+
<div style={{ maxHeight: '66vh', overflowY: 'auto', overflowX: 'hidden' }}>
|
|
438
|
+
{!props.dataLoaded ? <Spinner style={{ padding: '20px' }} /> : (
|
|
439
|
+
<DataGridBody>
|
|
440
|
+
{({ item, rowId }) => (
|
|
441
|
+
<DataGridRow className={selectedItemId === rowId ? dataGridRowStyles : ''} key={rowId} selectionCell={{ "aria-label": "Select row" }}>
|
|
442
|
+
{({ renderCell }) => <DataGridCell>{renderCell(item)}</DataGridCell>}
|
|
443
|
+
</DataGridRow>
|
|
444
|
+
)}
|
|
445
|
+
</DataGridBody>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
</DataGrid>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
type IHHMIDataGridVirtualizedProps = DataGridProps & {
|
|
454
|
+
dataLoaded: boolean;
|
|
455
|
+
height?: string;
|
|
456
|
+
itemSize?: number;
|
|
457
|
+
customGridParentWidth?: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function HHMIDataGridVirtualized(props: IHHMIDataGridVirtualizedProps) {
|
|
461
|
+
const dataGrid = makeStyles({ dataGridRowSelected: { color: tokens.colorBrandBackgroundInverted } });
|
|
462
|
+
const dataGridRowStyles = dataGrid().dataGridRowSelected;
|
|
463
|
+
const selectedItemId: number | undefined = props.selectedItems ? Array.from(props.selectedItems)[0] as number : undefined;
|
|
464
|
+
const parseHeight = (heightStr?: string): number => {
|
|
465
|
+
if (!heightStr) return 500;
|
|
466
|
+
if (heightStr.endsWith('px')) return parseInt(heightStr, 10);
|
|
467
|
+
if (heightStr.endsWith('vh')) return Math.floor((window.innerHeight * parseFloat(heightStr)) / 100);
|
|
468
|
+
return 500;
|
|
469
|
+
};
|
|
470
|
+
const containerHeight = parseHeight(props.height);
|
|
471
|
+
const rowHeight = props.itemSize || 44;
|
|
472
|
+
return (
|
|
473
|
+
<div style={{ width: 'fit-content' }}>
|
|
474
|
+
<DataGrid {...props} style={{ overflow: 'visible', width: props.customGridParentWidth }}>
|
|
475
|
+
<DataGridHeader>
|
|
476
|
+
<DataGridRow {...(props.selectionMode !== undefined ? { selectionCell: { "aria-label": "Select all rows" } } : {})}>
|
|
477
|
+
{({ renderHeaderCell }) => <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>}
|
|
478
|
+
</DataGridRow>
|
|
479
|
+
</DataGridHeader>
|
|
480
|
+
{!props.dataLoaded ? (
|
|
481
|
+
<div style={{ height: containerHeight, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spinner /></div>
|
|
482
|
+
) : (
|
|
483
|
+
<DataGridBodyVirtualized height={containerHeight} itemSize={rowHeight} width="100%">
|
|
484
|
+
{(row, style, index) => {
|
|
485
|
+
const rowId = index;
|
|
486
|
+
const isSelected = selectedItemId === rowId;
|
|
487
|
+
const item = (row as { item: unknown }).item;
|
|
488
|
+
return (
|
|
489
|
+
<div style={style}>
|
|
490
|
+
<DataGridRow className={isSelected ? dataGridRowStyles : ''} key={rowId} {...(props.selectionMode !== undefined ? { selectionCell: { "aria-label": "Select row" } } : {})}>
|
|
491
|
+
{({ renderCell }) => <DataGridCell>{renderCell(item)}</DataGridCell>}
|
|
492
|
+
</DataGridRow>
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}}
|
|
496
|
+
</DataGridBodyVirtualized>
|
|
497
|
+
)}
|
|
498
|
+
</DataGrid>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface IHHMIHomeIconProps { icons: IHHMIHomeIcon[]; }
|
|
504
|
+
|
|
505
|
+
export function HHMIHomeIcons(props: IHHMIHomeIconProps) {
|
|
506
|
+
const iconStyles = makeStyles({
|
|
507
|
+
icon: {
|
|
508
|
+
width: '175px',
|
|
509
|
+
height: '175px',
|
|
510
|
+
backgroundColor: tokens.colorBrandBackground,
|
|
511
|
+
display: 'flex',
|
|
512
|
+
flexDirection: 'column',
|
|
513
|
+
alignItems: 'center',
|
|
514
|
+
justifyContent: 'center',
|
|
515
|
+
...shorthands.borderRadius('20px'),
|
|
516
|
+
cursor: 'pointer',
|
|
517
|
+
...shorthands.margin('0', '20px'),
|
|
518
|
+
'&:hover': { backgroundColor: tokens.colorBrandBackgroundHover }
|
|
519
|
+
},
|
|
520
|
+
text: {
|
|
521
|
+
color: "#fff",
|
|
522
|
+
fontSize: tokens.fontSizeBase500,
|
|
523
|
+
textAlign: 'center',
|
|
524
|
+
lineHeight: '1.2',
|
|
525
|
+
...shorthands.margin('6px', '0', '0', '0'),
|
|
526
|
+
...shorthands.padding('0', '5px')
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const styles = iconStyles();
|
|
530
|
+
return (
|
|
531
|
+
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '50px' }}>
|
|
532
|
+
{props.icons.map((icon: IHHMIHomeIcon) => (
|
|
533
|
+
<div key={icon.key} className={styles.icon} onClick={icon.onClick}>
|
|
534
|
+
<div style={{ color: "#fff", fontSize: tokens.fontSizeHero1000 }}>{icon.icon}</div>
|
|
535
|
+
<div className={styles.text}>{icon.text}</div>
|
|
536
|
+
</div>
|
|
537
|
+
))}
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function HHMITypeAhead(props: {
|
|
543
|
+
options: { text: string; value: string; }[];
|
|
544
|
+
allowMultiple: boolean;
|
|
545
|
+
initialSelectedValue: string;
|
|
546
|
+
onOptionSelect: ((event: SelectionEvents, data: OptionOnSelectData) => void);
|
|
547
|
+
placeholder?: string;
|
|
548
|
+
}) {
|
|
549
|
+
const comboId = useId();
|
|
550
|
+
const [matchingOptions, setMatchingOptions] = React.useState([...props.options]);
|
|
551
|
+
const [customSearch, setCustomSearch] = React.useState<string>(props.initialSelectedValue);
|
|
552
|
+
const onChange: ComboboxProps["onChange"] = (event) => {
|
|
553
|
+
const value = event.target.value.trim();
|
|
554
|
+
setMatchingOptions(value.trim().length === 0 ? [...props.options] : props.options.filter((o) => o.text.toLowerCase().includes(value.toLowerCase())));
|
|
555
|
+
setCustomSearch(value);
|
|
556
|
+
};
|
|
557
|
+
const onOptionSelect: ComboboxProps["onOptionSelect"] = (_event, data) => {
|
|
558
|
+
if (data && data.optionText) setCustomSearch(data.optionText);
|
|
559
|
+
props.onOptionSelect(_event, data);
|
|
560
|
+
};
|
|
561
|
+
return (
|
|
562
|
+
<Combobox aria-labelledby={comboId} freeform placeholder={props.placeholder ? props.placeholder : 'Start typing to search ...'} onChange={onChange} onOptionSelect={onOptionSelect} value={customSearch}>
|
|
563
|
+
{customSearch ? <Option key="freeform" text={customSearch} disabled>Searching for "{customSearch}"</Option> : null}
|
|
564
|
+
{matchingOptions.map((option) => <Option key={option.value} value={option.value} text={option.text}>{option.text}</Option>)}
|
|
565
|
+
</Combobox>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import ReactQuill from 'react-quill';
|
|
2
|
+
import 'react-quill/dist/quill.snow.css';
|
|
3
|
+
import StringMap from 'quill';
|
|
4
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
import { tokens } from '@fluentui/react-components';
|
|
6
|
+
|
|
7
|
+
interface IReactQuillProps {
|
|
8
|
+
modules?: StringMap;
|
|
9
|
+
onChange: (content: string) => void;
|
|
10
|
+
value: string;
|
|
11
|
+
isDarkTheme: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const quillModules = {
|
|
15
|
+
toolbar: [
|
|
16
|
+
[{ 'header': [1, 2, false] }],
|
|
17
|
+
['bold', 'italic', 'underline', 'strike', 'blockquote'],
|
|
18
|
+
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'indent': '-1' }, { 'indent': '+1' }],
|
|
19
|
+
['link', 'image'],
|
|
20
|
+
['clean'],
|
|
21
|
+
[{ align: '' }, { align: 'center' }, { align: 'right' }, { align: 'justify' }],
|
|
22
|
+
[{ 'color': [] }, { 'background': [] }]
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const Quill = (props: IReactQuillProps): JSX.Element => {
|
|
27
|
+
const quillRef = useRef<ReactQuill>(null);
|
|
28
|
+
const styleId = 'quill-custom-styles';
|
|
29
|
+
|
|
30
|
+
const applyCustomStyles = useCallback(() => {
|
|
31
|
+
const customStyles = `
|
|
32
|
+
.ql-toolbar {
|
|
33
|
+
background-color: ${props.isDarkTheme ? tokens.colorNeutralBackgroundInverted : 'inherit'} !important;
|
|
34
|
+
}
|
|
35
|
+
.ql-container {
|
|
36
|
+
height: auto !important;
|
|
37
|
+
}`
|
|
38
|
+
;
|
|
39
|
+
|
|
40
|
+
const styleElement = document.createElement('style');
|
|
41
|
+
styleElement.id = styleId;
|
|
42
|
+
styleElement.textContent = customStyles;
|
|
43
|
+
document.head.appendChild(styleElement);
|
|
44
|
+
}, [props.isDarkTheme, styleId]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
applyCustomStyles();
|
|
48
|
+
}, [applyCustomStyles]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ReactQuill
|
|
52
|
+
ref={quillRef}
|
|
53
|
+
value={props.value}
|
|
54
|
+
onChange={props.onChange}
|
|
55
|
+
modules={props.modules ? props.modules : quillModules}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default Quill;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import App from './App.tsx'
|
|
4
|
+
import { BrowserRouter } from 'react-router';
|
|
5
|
+
import './index.css'
|
|
6
|
+
|
|
7
|
+
const rootElement = document.getElementById('root');
|
|
8
|
+
if (rootElement !== null) {
|
|
9
|
+
const root = createRoot(rootElement);
|
|
10
|
+
root.render(
|
|
11
|
+
<StrictMode>
|
|
12
|
+
<BrowserRouter>
|
|
13
|
+
<App />
|
|
14
|
+
</BrowserRouter>
|
|
15
|
+
</StrictMode>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { makeStyles, Text } from "@fluentui/react-components";
|
|
2
|
+
import type { IExampleUser } from "../../types/IExampleUser";
|
|
3
|
+
|
|
4
|
+
const useStyles = makeStyles({
|
|
5
|
+
container: {
|
|
6
|
+
display: "flex",
|
|
7
|
+
flexDirection: "column",
|
|
8
|
+
gap: "16px",
|
|
9
|
+
padding: "16px",
|
|
10
|
+
},
|
|
11
|
+
title: {
|
|
12
|
+
fontWeight: "600",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default function ExampleConfigurationPage(_props: { currentUser: IExampleUser }) {
|
|
17
|
+
const styles = useStyles();
|
|
18
|
+
return (
|
|
19
|
+
<div className={styles.container}>
|
|
20
|
+
<Text className={styles.title} as="h1">Configuration</Text>
|
|
21
|
+
<Text>Administration and configuration placeholder. Add your own settings or user management here.</Text>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|