@medplum/react 2.0.16 → 2.0.17

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.
Files changed (204) hide show
  1. package/dist/cjs/index.cjs +1546 -1064
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.min.cjs +1 -1
  4. package/dist/esm/AppShell/AppShell.mjs +37 -0
  5. package/dist/esm/AppShell/AppShell.mjs.map +1 -0
  6. package/dist/esm/AppShell/Header.mjs +88 -0
  7. package/dist/esm/AppShell/Header.mjs.map +1 -0
  8. package/dist/esm/AppShell/HeaderSearchInput.mjs +232 -0
  9. package/dist/esm/AppShell/HeaderSearchInput.mjs.map +1 -0
  10. package/dist/esm/AppShell/Navbar.mjs +113 -0
  11. package/dist/esm/AppShell/Navbar.mjs.map +1 -0
  12. package/dist/esm/GoogleButton/GoogleButton.mjs +2 -2
  13. package/dist/esm/GoogleButton/GoogleButton.mjs.map +1 -1
  14. package/dist/esm/Loading/Loading.mjs +10 -0
  15. package/dist/esm/Loading/Loading.mjs.map +1 -0
  16. package/dist/esm/MedplumLink/MedplumLink.mjs +1 -1
  17. package/dist/esm/MedplumLink/MedplumLink.mjs.map +1 -1
  18. package/dist/esm/SearchControl/SearchControl.mjs +3 -3
  19. package/dist/esm/SearchControl/SearchControl.mjs.map +1 -1
  20. package/dist/esm/auth/NewUserForm.mjs.map +1 -1
  21. package/dist/esm/auth/RegisterForm.mjs.map +1 -1
  22. package/dist/esm/index.min.mjs +1 -1
  23. package/dist/esm/index.mjs +7 -3
  24. package/dist/esm/index.mjs.map +1 -1
  25. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/createReactComponent.mjs +1 -1
  26. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/createReactComponent.mjs.map +1 -1
  27. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/defaultAttributes.mjs +1 -1
  28. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/defaultAttributes.mjs.map +1 -1
  29. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAdjustmentsHorizontal.mjs +1 -1
  30. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAdjustmentsHorizontal.mjs.map +1 -1
  31. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAlertCircle.mjs +1 -1
  32. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAlertCircle.mjs.map +1 -1
  33. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleach.mjs +1 -1
  34. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleach.mjs.map +1 -1
  35. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleachOff.mjs +1 -1
  36. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleachOff.mjs.map +1 -1
  37. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBoxMultiple.mjs +1 -1
  38. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBoxMultiple.mjs.map +1 -1
  39. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBracketsContain.mjs +1 -1
  40. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBracketsContain.mjs.map +1 -1
  41. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucket.mjs +1 -1
  42. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucket.mjs.map +1 -1
  43. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucketOff.mjs +1 -1
  44. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucketOff.mjs.map +1 -1
  45. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCalendar.mjs +1 -1
  46. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCalendar.mjs.map +1 -1
  47. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheck.mjs +1 -1
  48. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheck.mjs.map +1 -1
  49. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheckbox.mjs +1 -1
  50. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheckbox.mjs.map +1 -1
  51. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconChevronDown.mjs +12 -0
  52. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconChevronDown.mjs.map +1 -0
  53. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCircleMinus.mjs +1 -1
  54. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCircleMinus.mjs.map +1 -1
  55. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCirclePlus.mjs +1 -1
  56. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCirclePlus.mjs.map +1 -1
  57. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCloudUpload.mjs +1 -1
  58. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCloudUpload.mjs.map +1 -1
  59. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconColumns.mjs +1 -1
  60. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconColumns.mjs.map +1 -1
  61. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCurrencyDollar.mjs +1 -1
  62. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCurrencyDollar.mjs.map +1 -1
  63. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconDots.mjs +1 -1
  64. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconDots.mjs.map +1 -1
  65. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEdit.mjs +1 -1
  66. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEdit.mjs.map +1 -1
  67. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqual.mjs +1 -1
  68. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqual.mjs.map +1 -1
  69. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqualNot.mjs +1 -1
  70. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqualNot.mjs.map +1 -1
  71. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFileAlert.mjs +1 -1
  72. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFileAlert.mjs.map +1 -1
  73. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilePlus.mjs +1 -1
  74. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilePlus.mjs.map +1 -1
  75. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilter.mjs +1 -1
  76. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilter.mjs.map +1 -1
  77. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconListDetails.mjs +1 -1
  78. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconListDetails.mjs.map +1 -1
  79. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconLogout.mjs +19 -0
  80. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconLogout.mjs.map +1 -0
  81. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathGreater.mjs +1 -1
  82. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathGreater.mjs.map +1 -1
  83. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathLower.mjs +1 -1
  84. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathLower.mjs.map +1 -1
  85. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMessage.mjs +1 -1
  86. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMessage.mjs.map +1 -1
  87. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPin.mjs +1 -1
  88. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPin.mjs.map +1 -1
  89. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPinnedOff.mjs +1 -1
  90. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPinnedOff.mjs.map +1 -1
  91. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSearch.mjs +13 -0
  92. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSearch.mjs.map +1 -0
  93. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSettings.mjs +1 -1
  94. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSettings.mjs.map +1 -1
  95. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortAscending.mjs +1 -1
  96. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortAscending.mjs.map +1 -1
  97. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortDescending.mjs +1 -1
  98. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortDescending.mjs.map +1 -1
  99. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSquare.mjs +1 -1
  100. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSquare.mjs.map +1 -1
  101. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSwitchHorizontal.mjs +19 -0
  102. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSwitchHorizontal.mjs.map +1 -0
  103. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTableExport.mjs +1 -1
  104. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTableExport.mjs.map +1 -1
  105. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTrash.mjs +1 -1
  106. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTrash.mjs.map +1 -1
  107. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconX.mjs +1 -1
  108. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconX.mjs.map +1 -1
  109. package/dist/types/AddressDisplay/AddressDisplay.d.ts +0 -1
  110. package/dist/types/AddressInput/AddressInput.d.ts +0 -1
  111. package/dist/types/AnnotationInput/AnnotationInput.d.ts +0 -1
  112. package/dist/types/AppShell/AppShell.d.ts +9 -0
  113. package/dist/types/AppShell/Header.d.ts +8 -0
  114. package/dist/types/AppShell/HeaderSearchInput.d.ts +3 -0
  115. package/dist/types/AppShell/Navbar.d.ts +14 -0
  116. package/dist/types/AsyncAutocomplete/AsyncAutocomplete.d.ts +0 -1
  117. package/dist/types/AttachmentArrayDisplay/AttachmentArrayDisplay.d.ts +0 -1
  118. package/dist/types/AttachmentArrayInput/AttachmentArrayInput.d.ts +0 -1
  119. package/dist/types/AttachmentDisplay/AttachmentDisplay.d.ts +0 -1
  120. package/dist/types/AttachmentInput/AttachmentInput.d.ts +0 -1
  121. package/dist/types/BackboneElementDisplay/BackboneElementDisplay.d.ts +0 -1
  122. package/dist/types/BackboneElementInput/BackboneElementInput.d.ts +0 -1
  123. package/dist/types/CalendarInput/CalendarInput.d.ts +0 -1
  124. package/dist/types/CodeInput/CodeInput.d.ts +0 -1
  125. package/dist/types/CodeableConceptDisplay/CodeableConceptDisplay.d.ts +0 -1
  126. package/dist/types/CodeableConceptInput/CodeableConceptInput.d.ts +0 -1
  127. package/dist/types/CodingDisplay/CodingDisplay.d.ts +0 -1
  128. package/dist/types/CodingInput/CodingInput.d.ts +0 -1
  129. package/dist/types/ContactDetailDisplay/ContactDetailDisplay.d.ts +0 -1
  130. package/dist/types/ContactDetailInput/ContactDetailInput.d.ts +0 -1
  131. package/dist/types/ContactPointDisplay/ContactPointDisplay.d.ts +0 -1
  132. package/dist/types/ContactPointInput/ContactPointInput.d.ts +0 -1
  133. package/dist/types/Container/Container.d.ts +0 -1
  134. package/dist/types/DateTimeInput/DateTimeInput.d.ts +0 -1
  135. package/dist/types/DefaultResourceTimeline/DefaultResourceTimeline.d.ts +0 -1
  136. package/dist/types/DiagnosticReportDisplay/DiagnosticReportDisplay.d.ts +0 -1
  137. package/dist/types/Document/Document.d.ts +0 -1
  138. package/dist/types/EncounterTimeline/EncounterTimeline.d.ts +0 -1
  139. package/dist/types/ExtensionInput/ExtensionInput.d.ts +0 -1
  140. package/dist/types/FhirPathDisplay/FhirPathDisplay.d.ts +0 -1
  141. package/dist/types/GoogleButton/GoogleButton.d.ts +0 -1
  142. package/dist/types/HumanNameDisplay/HumanNameDisplay.d.ts +0 -1
  143. package/dist/types/HumanNameInput/HumanNameInput.d.ts +0 -1
  144. package/dist/types/IdentifierDisplay/IdentifierDisplay.d.ts +0 -1
  145. package/dist/types/IdentifierInput/IdentifierInput.d.ts +0 -1
  146. package/dist/types/Loading/Loading.d.ts +1 -0
  147. package/dist/types/Logo/Logo.d.ts +0 -1
  148. package/dist/types/MedplumLink/MedplumLink.d.ts +1 -1
  149. package/dist/types/MoneyDisplay/MoneyDisplay.d.ts +0 -1
  150. package/dist/types/MoneyInput/MoneyInput.d.ts +0 -1
  151. package/dist/types/NoteDisplay/NoteDisplay.d.ts +0 -1
  152. package/dist/types/OperationOutcomeAlert/OperationOutcomeAlert.d.ts +0 -1
  153. package/dist/types/Panel/Panel.d.ts +0 -1
  154. package/dist/types/PatientTimeline/PatientTimeline.d.ts +0 -1
  155. package/dist/types/PeriodInput/PeriodInput.d.ts +0 -1
  156. package/dist/types/PlanDefinitionBuilder/PlanDefinitionBuilder.d.ts +0 -1
  157. package/dist/types/QuantityDisplay/QuantityDisplay.d.ts +0 -1
  158. package/dist/types/QuantityInput/QuantityInput.d.ts +0 -1
  159. package/dist/types/QuestionnaireBuilder/QuestionnaireBuilder.d.ts +0 -1
  160. package/dist/types/QuestionnaireForm/QuestionnaireForm.d.ts +0 -1
  161. package/dist/types/RangeDisplay/RangeDisplay.d.ts +0 -1
  162. package/dist/types/RangeInput/RangeInput.d.ts +0 -1
  163. package/dist/types/RatioDisplay/RatioDisplay.d.ts +0 -1
  164. package/dist/types/RatioInput/RatioInput.d.ts +0 -1
  165. package/dist/types/ReferenceDisplay/ReferenceDisplay.d.ts +0 -1
  166. package/dist/types/ReferenceInput/ReferenceInput.d.ts +0 -1
  167. package/dist/types/ReferenceRangeEditor/ReferenceRangeEditor.d.ts +0 -1
  168. package/dist/types/RequestGroupDisplay/RequestGroupDisplay.d.ts +0 -1
  169. package/dist/types/ResourceArrayDisplay/ResourceArrayDisplay.d.ts +0 -1
  170. package/dist/types/ResourceArrayInput/ResourceArrayInput.d.ts +0 -1
  171. package/dist/types/ResourceAvatar/ResourceAvatar.d.ts +0 -1
  172. package/dist/types/ResourceBadge/ResourceBadge.d.ts +0 -1
  173. package/dist/types/ResourceBlame/ResourceBlame.d.ts +0 -1
  174. package/dist/types/ResourceDiff/ResourceDiff.d.ts +0 -1
  175. package/dist/types/ResourceDiffTable/ResourceDiffTable.d.ts +0 -1
  176. package/dist/types/ResourceForm/ResourceForm.d.ts +0 -1
  177. package/dist/types/ResourceHistoryTable/ResourceHistoryTable.d.ts +0 -1
  178. package/dist/types/ResourceInput/ResourceInput.d.ts +0 -1
  179. package/dist/types/ResourceName/ResourceName.d.ts +0 -1
  180. package/dist/types/ResourcePropertyDisplay/ResourcePropertyDisplay.d.ts +0 -1
  181. package/dist/types/ResourcePropertyInput/ResourcePropertyInput.d.ts +0 -1
  182. package/dist/types/ResourceTable/ResourceTable.d.ts +0 -1
  183. package/dist/types/ResourceTimeline/ResourceTimeline.d.ts +0 -1
  184. package/dist/types/Scheduler/Scheduler.d.ts +0 -1
  185. package/dist/types/SearchControl/SearchUtils.d.ts +0 -1
  186. package/dist/types/SearchExportDialog/SearchExportDialog.d.ts +0 -1
  187. package/dist/types/SearchFieldEditor/SearchFieldEditor.d.ts +0 -1
  188. package/dist/types/SearchFilterEditor/SearchFilterEditor.d.ts +0 -1
  189. package/dist/types/SearchFilterValueDialog/SearchFilterValueDialog.d.ts +0 -1
  190. package/dist/types/SearchFilterValueDisplay/SearchFilterValueDisplay.d.ts +0 -1
  191. package/dist/types/SearchFilterValueInput/SearchFilterValueInput.d.ts +0 -1
  192. package/dist/types/SearchPopupMenu/SearchPopupMenu.d.ts +0 -1
  193. package/dist/types/ServiceRequestTimeline/ServiceRequestTimeline.d.ts +0 -1
  194. package/dist/types/StatusBadge/StatusBadge.d.ts +0 -1
  195. package/dist/types/TimingInput/TimingInput.d.ts +0 -1
  196. package/dist/types/ValueSetAutocomplete/ValueSetAutocomplete.d.ts +0 -1
  197. package/dist/types/auth/ChooseProfileForm.d.ts +0 -1
  198. package/dist/types/auth/ChooseScopeForm.d.ts +0 -1
  199. package/dist/types/auth/MfaForm.d.ts +0 -1
  200. package/dist/types/auth/NewProjectForm.d.ts +0 -1
  201. package/dist/types/auth/NewUserForm.d.ts +1 -1
  202. package/dist/types/auth/RegisterForm.d.ts +1 -1
  203. package/dist/types/index.d.ts +7 -3
  204. package/package.json +10 -10
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@medplum/core'), require('react'), require('@mantine/core'), require('prop-types'), require('@mantine/notifications')) :
3
- typeof define === 'function' && define.amd ? define(['exports', '@medplum/core', 'react', '@mantine/core', 'prop-types', '@mantine/notifications'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.medplum = global.medplum || {}, global.medplum.react = {}), global.medplum.core, global.React, global.mantine.core, global.PropTypes, global.mantine.notifications));
5
- })(this, (function (exports, core, React, core$1, PropTypes, notifications) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@medplum/core'), require('react'), require('@mantine/core'), require('prop-types'), require('react-router-dom'), require('@mantine/notifications')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', '@medplum/core', 'react', '@mantine/core', 'prop-types', 'react-router-dom', '@mantine/notifications'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.medplum = global.medplum || {}, global.medplum.react = {}), global.medplum.core, global.React, global.mantine.core, global.PropTypes, global.ReactRouterDOM, global.mantine.notifications));
5
+ })(this, (function (exports, core, React, core$1, PropTypes, reactRouterDom, notifications) { 'use strict';
6
6
 
7
7
  function AddressDisplay(props) {
8
8
  const address = props.value;
@@ -154,169 +154,7 @@
154
154
  }
155
155
 
156
156
  /**
157
- * Kills a browser event.
158
- * Prevents default behavior.
159
- * Stops event propagation.
160
- * @param e The event.
161
- */
162
- function killEvent(e) {
163
- e.preventDefault();
164
- e.stopPropagation();
165
- }
166
- /**
167
- * Returns true if the element is a checkbox or a table cell containing a checkbox.
168
- * Table cells containing checkboxes are commonly accidentally clicked.
169
- * @param el The HTML DOM element.
170
- * @returns True if the element is a checkbox or a table cell containing a checkbox.
171
- */
172
- function isCheckboxCell(el) {
173
- if (isCheckboxElement(el)) {
174
- return true;
175
- }
176
- if (el instanceof HTMLTableCellElement) {
177
- const children = el.children;
178
- if (children.length === 1 && isCheckboxElement(children[0])) {
179
- return true;
180
- }
181
- }
182
- return false;
183
- }
184
- function isCheckboxElement(el) {
185
- return el instanceof HTMLInputElement && el.type === 'checkbox';
186
- }
187
-
188
- function AsyncAutocomplete(props) {
189
- const { defaultValue, toKey, toOption, loadOptions, onChange, onCreate, creatable, ...rest } = props;
190
- const defaultItems = toDefaultItems(defaultValue);
191
- const inputRef = React.useRef(null);
192
- const [lastValue, setLastValue] = React.useState(undefined);
193
- const [timer, setTimer] = React.useState();
194
- const [abortController, setAbortController] = React.useState();
195
- const [autoSubmit, setAutoSubmit] = React.useState();
196
- const [options, setOptions] = React.useState(defaultItems?.map(toOption));
197
- const lastValueRef = React.useRef();
198
- lastValueRef.current = lastValue;
199
- const timerRef = React.useRef();
200
- timerRef.current = timer;
201
- const abortControllerRef = React.useRef();
202
- abortControllerRef.current = abortController;
203
- const autoSubmitRef = React.useRef();
204
- autoSubmitRef.current = autoSubmit;
205
- const optionsRef = React.useRef();
206
- optionsRef.current = options;
207
- const handleTimer = React.useCallback(() => {
208
- setTimer(undefined);
209
- const value = inputRef.current?.value?.trim() || '';
210
- if (value === lastValueRef.current) {
211
- // Nothing has changed, move on
212
- return;
213
- }
214
- setLastValue(value);
215
- const newAbortController = new AbortController();
216
- setAbortController(newAbortController);
217
- loadOptions(value, newAbortController.signal)
218
- .then((newValues) => {
219
- if (!newAbortController.signal.aborted) {
220
- setOptions(newValues.map(toOption));
221
- setAbortController(undefined);
222
- if (autoSubmitRef.current) {
223
- if (newValues.length > 0) {
224
- onChange(newValues.slice(0, 1));
225
- }
226
- setAutoSubmit(false);
227
- }
228
- }
229
- })
230
- .catch(console.log);
231
- }, [loadOptions, onChange, toOption]);
232
- const handleSearchChange = React.useCallback(() => {
233
- if (abortControllerRef.current) {
234
- abortControllerRef.current.abort();
235
- setAbortController(undefined);
236
- }
237
- if (timerRef.current !== undefined) {
238
- window.clearTimeout(timerRef.current);
239
- }
240
- const newTimer = window.setTimeout(() => handleTimer(), 100);
241
- setTimer(newTimer);
242
- }, [handleTimer]);
243
- const handleChange = React.useCallback((values) => {
244
- const result = [];
245
- for (const value of values) {
246
- let item = optionsRef.current?.find((option) => option.value === value)?.resource;
247
- if (!item && creatable !== false) {
248
- item = onCreate(value);
249
- }
250
- if (item)
251
- result.push(item);
252
- }
253
- onChange(result);
254
- }, [creatable, onChange, onCreate]);
255
- const handleKeyDown = React.useCallback((e) => {
256
- if (e.key === 'Enter') {
257
- if (!timerRef.current && !abortControllerRef.current) {
258
- killEvent(e);
259
- if (optionsRef.current && optionsRef.current.length > 0) {
260
- setOptions(optionsRef.current.slice(0, 1));
261
- handleChange([optionsRef.current[0].value]);
262
- }
263
- }
264
- else {
265
- // The user pressed enter, but we don't have results yet.
266
- // We need to wait for the results to come in.
267
- setAutoSubmit(true);
268
- }
269
- }
270
- }, [handleChange]);
271
- const handleCreate = React.useCallback((input) => {
272
- const option = toOption(onCreate(input));
273
- setOptions([...optionsRef.current, option]);
274
- return option;
275
- }, [onCreate, setOptions, toOption]);
276
- const handleFilter = React.useCallback((_value, selected) => !selected, []);
277
- React.useEffect(() => {
278
- return () => {
279
- if (abortControllerRef.current) {
280
- abortControllerRef.current.abort();
281
- }
282
- };
283
- }, []);
284
- return (React.createElement(core$1.MultiSelect, { ...rest, ref: inputRef, defaultValue: defaultItems.map(toKey), searchable: true, onKeyDown: handleKeyDown, onSearchChange: handleSearchChange, data: options, onFocus: handleTimer, onChange: handleChange, onCreate: handleCreate, rightSectionWidth: 40, rightSection: abortController ? React.createElement(core$1.Loader, { size: 16 }) : null, filter: handleFilter, creatable: true }));
285
- }
286
- function toDefaultItems(defaultValue) {
287
- if (!defaultValue) {
288
- return [];
289
- }
290
- if (Array.isArray(defaultValue)) {
291
- return defaultValue;
292
- }
293
- return [defaultValue];
294
- }
295
-
296
- function AttachmentDisplay(props) {
297
- const value = props.value;
298
- const { contentType, url, title } = value ?? {};
299
- if (!url) {
300
- return null;
301
- }
302
- return (React.createElement("div", { "data-testid": "attachment-display" },
303
- contentType?.startsWith('image/') && (React.createElement("img", { "data-testid": "attachment-image", style: { maxWidth: props.maxWidth }, src: url, alt: value?.title })),
304
- contentType?.startsWith('video/') && (React.createElement("video", { "data-testid": "attachment-video", style: { maxWidth: props.maxWidth }, controls: true },
305
- React.createElement("source", { type: contentType, src: url }))),
306
- contentType === 'application/pdf' && !title?.endsWith('.pdf') && (React.createElement("div", { "data-testid": "attachment-pdf", style: { maxWidth: props.maxWidth, minHeight: 400 } },
307
- React.createElement("iframe", { width: "100%", height: "400", src: url + '#navpanes=0', allowFullScreen: true, frameBorder: 0, seamless: true }))),
308
- React.createElement("div", { "data-testid": "download-link", style: { padding: '2px 16px 16px 16px' } },
309
- React.createElement(core$1.Anchor, { href: value?.url, "data-testid": "attachment-details", target: "_blank", rel: "noopener noreferrer" }, value?.title || 'Download'))));
310
- }
311
-
312
- function AttachmentArrayDisplay(props) {
313
- return (React.createElement("div", null, props.values &&
314
- props.values.map((v, index) => (React.createElement("div", { key: 'attatchment-' + index },
315
- React.createElement(AttachmentDisplay, { value: v, maxWidth: props.maxWidth }))))));
316
- }
317
-
318
- /**
319
- * @tabler/icons-react v2.16.0 - MIT
157
+ * @tabler/icons-react v2.17.0 - MIT
320
158
  */
321
159
 
322
160
  var defaultAttributes = {
@@ -332,7 +170,7 @@
332
170
  };
333
171
 
334
172
  /**
335
- * @tabler/icons-react v2.16.0 - MIT
173
+ * @tabler/icons-react v2.17.0 - MIT
336
174
  */
337
175
 
338
176
  var __defProp = Object.defineProperty;
@@ -395,7 +233,7 @@
395
233
  };
396
234
 
397
235
  /**
398
- * @tabler/icons-react v2.16.0 - MIT
236
+ * @tabler/icons-react v2.17.0 - MIT
399
237
  */
400
238
 
401
239
  var IconAdjustmentsHorizontal = createReactComponent(
@@ -415,7 +253,7 @@
415
253
  );
416
254
 
417
255
  /**
418
- * @tabler/icons-react v2.16.0 - MIT
256
+ * @tabler/icons-react v2.17.0 - MIT
419
257
  */
420
258
 
421
259
  var IconAlertCircle = createReactComponent("alert-circle", "IconAlertCircle", [
@@ -425,7 +263,7 @@
425
263
  ]);
426
264
 
427
265
  /**
428
- * @tabler/icons-react v2.16.0 - MIT
266
+ * @tabler/icons-react v2.17.0 - MIT
429
267
  */
430
268
 
431
269
  var IconBleachOff = createReactComponent("bleach-off", "IconBleachOff", [
@@ -440,7 +278,7 @@
440
278
  ]);
441
279
 
442
280
  /**
443
- * @tabler/icons-react v2.16.0 - MIT
281
+ * @tabler/icons-react v2.17.0 - MIT
444
282
  */
445
283
 
446
284
  var IconBleach = createReactComponent("bleach", "IconBleach", [
@@ -454,7 +292,7 @@
454
292
  ]);
455
293
 
456
294
  /**
457
- * @tabler/icons-react v2.16.0 - MIT
295
+ * @tabler/icons-react v2.17.0 - MIT
458
296
  */
459
297
 
460
298
  var IconBoxMultiple = createReactComponent("box-multiple", "IconBoxMultiple", [
@@ -475,7 +313,7 @@
475
313
  ]);
476
314
 
477
315
  /**
478
- * @tabler/icons-react v2.16.0 - MIT
316
+ * @tabler/icons-react v2.17.0 - MIT
479
317
  */
480
318
 
481
319
  var IconBracketsContain = createReactComponent("brackets-contain", "IconBracketsContain", [
@@ -487,7 +325,7 @@
487
325
  ]);
488
326
 
489
327
  /**
490
- * @tabler/icons-react v2.16.0 - MIT
328
+ * @tabler/icons-react v2.17.0 - MIT
491
329
  */
492
330
 
493
331
  var IconBucketOff = createReactComponent("bucket-off", "IconBucketOff", [
@@ -509,7 +347,7 @@
509
347
  ]);
510
348
 
511
349
  /**
512
- * @tabler/icons-react v2.16.0 - MIT
350
+ * @tabler/icons-react v2.17.0 - MIT
513
351
  */
514
352
 
515
353
  var IconBucket = createReactComponent("bucket", "IconBucket", [
@@ -524,7 +362,7 @@
524
362
  ]);
525
363
 
526
364
  /**
527
- * @tabler/icons-react v2.16.0 - MIT
365
+ * @tabler/icons-react v2.17.0 - MIT
528
366
  */
529
367
 
530
368
  var IconCalendar = createReactComponent("calendar", "IconCalendar", [
@@ -543,7 +381,7 @@
543
381
  ]);
544
382
 
545
383
  /**
546
- * @tabler/icons-react v2.16.0 - MIT
384
+ * @tabler/icons-react v2.17.0 - MIT
547
385
  */
548
386
 
549
387
  var IconCheck = createReactComponent("check", "IconCheck", [
@@ -551,7 +389,7 @@
551
389
  ]);
552
390
 
553
391
  /**
554
- * @tabler/icons-react v2.16.0 - MIT
392
+ * @tabler/icons-react v2.17.0 - MIT
555
393
  */
556
394
 
557
395
  var IconCheckbox = createReactComponent("checkbox", "IconCheckbox", [
@@ -566,7 +404,15 @@
566
404
  ]);
567
405
 
568
406
  /**
569
- * @tabler/icons-react v2.16.0 - MIT
407
+ * @tabler/icons-react v2.17.0 - MIT
408
+ */
409
+
410
+ var IconChevronDown = createReactComponent("chevron-down", "IconChevronDown", [
411
+ ["path", { d: "M6 9l6 6l6 -6", key: "svg-0" }]
412
+ ]);
413
+
414
+ /**
415
+ * @tabler/icons-react v2.17.0 - MIT
570
416
  */
571
417
 
572
418
  var IconCircleMinus = createReactComponent("circle-minus", "IconCircleMinus", [
@@ -575,7 +421,7 @@
575
421
  ]);
576
422
 
577
423
  /**
578
- * @tabler/icons-react v2.16.0 - MIT
424
+ * @tabler/icons-react v2.17.0 - MIT
579
425
  */
580
426
 
581
427
  var IconCirclePlus = createReactComponent("circle-plus", "IconCirclePlus", [
@@ -585,7 +431,7 @@
585
431
  ]);
586
432
 
587
433
  /**
588
- * @tabler/icons-react v2.16.0 - MIT
434
+ * @tabler/icons-react v2.17.0 - MIT
589
435
  */
590
436
 
591
437
  var IconCloudUpload = createReactComponent("cloud-upload", "IconCloudUpload", [
@@ -601,7 +447,7 @@
601
447
  ]);
602
448
 
603
449
  /**
604
- * @tabler/icons-react v2.16.0 - MIT
450
+ * @tabler/icons-react v2.17.0 - MIT
605
451
  */
606
452
 
607
453
  var IconColumns = createReactComponent("columns", "IconColumns", [
@@ -616,7 +462,7 @@
616
462
  ]);
617
463
 
618
464
  /**
619
- * @tabler/icons-react v2.16.0 - MIT
465
+ * @tabler/icons-react v2.17.0 - MIT
620
466
  */
621
467
 
622
468
  var IconCurrencyDollar = createReactComponent("currency-dollar", "IconCurrencyDollar", [
@@ -631,7 +477,7 @@
631
477
  ]);
632
478
 
633
479
  /**
634
- * @tabler/icons-react v2.16.0 - MIT
480
+ * @tabler/icons-react v2.17.0 - MIT
635
481
  */
636
482
 
637
483
  var IconDots = createReactComponent("dots", "IconDots", [
@@ -641,7 +487,7 @@
641
487
  ]);
642
488
 
643
489
  /**
644
- * @tabler/icons-react v2.16.0 - MIT
490
+ * @tabler/icons-react v2.17.0 - MIT
645
491
  */
646
492
 
647
493
  var IconEdit = createReactComponent("edit", "IconEdit", [
@@ -663,7 +509,7 @@
663
509
  ]);
664
510
 
665
511
  /**
666
- * @tabler/icons-react v2.16.0 - MIT
512
+ * @tabler/icons-react v2.17.0 - MIT
667
513
  */
668
514
 
669
515
  var IconEqualNot = createReactComponent("equal-not", "IconEqualNot", [
@@ -673,7 +519,7 @@
673
519
  ]);
674
520
 
675
521
  /**
676
- * @tabler/icons-react v2.16.0 - MIT
522
+ * @tabler/icons-react v2.17.0 - MIT
677
523
  */
678
524
 
679
525
  var IconEqual = createReactComponent("equal", "IconEqual", [
@@ -682,7 +528,7 @@
682
528
  ]);
683
529
 
684
530
  /**
685
- * @tabler/icons-react v2.16.0 - MIT
531
+ * @tabler/icons-react v2.17.0 - MIT
686
532
  */
687
533
 
688
534
  var IconFileAlert = createReactComponent("file-alert", "IconFileAlert", [
@@ -699,7 +545,7 @@
699
545
  ]);
700
546
 
701
547
  /**
702
- * @tabler/icons-react v2.16.0 - MIT
548
+ * @tabler/icons-react v2.17.0 - MIT
703
549
  */
704
550
 
705
551
  var IconFilePlus = createReactComponent("file-plus", "IconFilePlus", [
@@ -716,7 +562,7 @@
716
562
  ]);
717
563
 
718
564
  /**
719
- * @tabler/icons-react v2.16.0 - MIT
565
+ * @tabler/icons-react v2.17.0 - MIT
720
566
  */
721
567
 
722
568
  var IconFilter = createReactComponent("filter", "IconFilter", [
@@ -730,7 +576,7 @@
730
576
  ]);
731
577
 
732
578
  /**
733
- * @tabler/icons-react v2.16.0 - MIT
579
+ * @tabler/icons-react v2.17.0 - MIT
734
580
  */
735
581
 
736
582
  var IconListDetails = createReactComponent("list-details", "IconListDetails", [
@@ -755,7 +601,22 @@
755
601
  ]);
756
602
 
757
603
  /**
758
- * @tabler/icons-react v2.16.0 - MIT
604
+ * @tabler/icons-react v2.17.0 - MIT
605
+ */
606
+
607
+ var IconLogout = createReactComponent("logout", "IconLogout", [
608
+ [
609
+ "path",
610
+ {
611
+ d: "M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2",
612
+ key: "svg-0"
613
+ }
614
+ ],
615
+ ["path", { d: "M7 12h14l-3 -3m0 6l3 -3", key: "svg-1" }]
616
+ ]);
617
+
618
+ /**
619
+ * @tabler/icons-react v2.17.0 - MIT
759
620
  */
760
621
 
761
622
  var IconMathGreater = createReactComponent("math-greater", "IconMathGreater", [
@@ -763,7 +624,7 @@
763
624
  ]);
764
625
 
765
626
  /**
766
- * @tabler/icons-react v2.16.0 - MIT
627
+ * @tabler/icons-react v2.17.0 - MIT
767
628
  */
768
629
 
769
630
  var IconMathLower = createReactComponent("math-lower", "IconMathLower", [
@@ -771,7 +632,7 @@
771
632
  ]);
772
633
 
773
634
  /**
774
- * @tabler/icons-react v2.16.0 - MIT
635
+ * @tabler/icons-react v2.17.0 - MIT
775
636
  */
776
637
 
777
638
  var IconMessage = createReactComponent("message", "IconMessage", [
@@ -787,7 +648,7 @@
787
648
  ]);
788
649
 
789
650
  /**
790
- * @tabler/icons-react v2.16.0 - MIT
651
+ * @tabler/icons-react v2.17.0 - MIT
791
652
  */
792
653
 
793
654
  var IconPin = createReactComponent("pin", "IconPin", [
@@ -803,7 +664,7 @@
803
664
  ]);
804
665
 
805
666
  /**
806
- * @tabler/icons-react v2.16.0 - MIT
667
+ * @tabler/icons-react v2.17.0 - MIT
807
668
  */
808
669
 
809
670
  var IconPinnedOff = createReactComponent("pinned-off", "IconPinnedOff", [
@@ -820,7 +681,16 @@
820
681
  ]);
821
682
 
822
683
  /**
823
- * @tabler/icons-react v2.16.0 - MIT
684
+ * @tabler/icons-react v2.17.0 - MIT
685
+ */
686
+
687
+ var IconSearch = createReactComponent("search", "IconSearch", [
688
+ ["path", { d: "M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0", key: "svg-0" }],
689
+ ["path", { d: "M21 21l-6 -6", key: "svg-1" }]
690
+ ]);
691
+
692
+ /**
693
+ * @tabler/icons-react v2.17.0 - MIT
824
694
  */
825
695
 
826
696
  var IconSettings = createReactComponent("settings", "IconSettings", [
@@ -835,7 +705,7 @@
835
705
  ]);
836
706
 
837
707
  /**
838
- * @tabler/icons-react v2.16.0 - MIT
708
+ * @tabler/icons-react v2.17.0 - MIT
839
709
  */
840
710
 
841
711
  var IconSortAscending = createReactComponent("sort-ascending", "IconSortAscending", [
@@ -847,7 +717,7 @@
847
717
  ]);
848
718
 
849
719
  /**
850
- * @tabler/icons-react v2.16.0 - MIT
720
+ * @tabler/icons-react v2.17.0 - MIT
851
721
  */
852
722
 
853
723
  var IconSortDescending = createReactComponent("sort-descending", "IconSortDescending", [
@@ -859,7 +729,7 @@
859
729
  ]);
860
730
 
861
731
  /**
862
- * @tabler/icons-react v2.16.0 - MIT
732
+ * @tabler/icons-react v2.17.0 - MIT
863
733
  */
864
734
 
865
735
  var IconSquare = createReactComponent("square", "IconSquare", [
@@ -873,7 +743,22 @@
873
743
  ]);
874
744
 
875
745
  /**
876
- * @tabler/icons-react v2.16.0 - MIT
746
+ * @tabler/icons-react v2.17.0 - MIT
747
+ */
748
+
749
+ var IconSwitchHorizontal = createReactComponent(
750
+ "switch-horizontal",
751
+ "IconSwitchHorizontal",
752
+ [
753
+ ["path", { d: "M16 3l4 4l-4 4", key: "svg-0" }],
754
+ ["path", { d: "M10 7l10 0", key: "svg-1" }],
755
+ ["path", { d: "M8 13l-4 4l4 4", key: "svg-2" }],
756
+ ["path", { d: "M4 17l9 0", key: "svg-3" }]
757
+ ]
758
+ );
759
+
760
+ /**
761
+ * @tabler/icons-react v2.17.0 - MIT
877
762
  */
878
763
 
879
764
  var IconTableExport = createReactComponent("table-export", "IconTableExport", [
@@ -891,7 +776,7 @@
891
776
  ]);
892
777
 
893
778
  /**
894
- * @tabler/icons-react v2.16.0 - MIT
779
+ * @tabler/icons-react v2.17.0 - MIT
895
780
  */
896
781
 
897
782
  var IconTrash = createReactComponent("trash", "IconTrash", [
@@ -906,7 +791,7 @@
906
791
  ]);
907
792
 
908
793
  /**
909
- * @tabler/icons-react v2.16.0 - MIT
794
+ * @tabler/icons-react v2.17.0 - MIT
910
795
  */
911
796
 
912
797
  var IconX = createReactComponent("x", "IconX", [
@@ -914,730 +799,936 @@
914
799
  ["path", { d: "M6 6l12 12", key: "svg-1" }]
915
800
  ]);
916
801
 
917
- function AttachmentButton(props) {
918
- const medplum = useMedplum();
919
- const fileInputRef = React.useRef(null);
920
- function onClick(e) {
921
- killEvent(e);
922
- fileInputRef.current?.click();
802
+ /**
803
+ * ErrorBoundary is a React component that handles errors in its child components.
804
+ * See: https://reactjs.org/docs/error-boundaries.html
805
+ */
806
+ class ErrorBoundary extends React.Component {
807
+ constructor(props) {
808
+ super(props);
809
+ this.state = {};
923
810
  }
924
- function onFileChange(e) {
925
- killEvent(e);
926
- const files = e.target.files;
927
- if (files) {
928
- Array.from(files).forEach(processFile);
929
- }
811
+ static getDerivedStateFromError(error) {
812
+ return { error };
930
813
  }
931
- /**
932
- * Processes a single file.
933
- *
934
- * @param {File} file The file descriptor.
935
- */
936
- function processFile(file) {
937
- if (!file) {
938
- return;
939
- }
940
- const fileName = file.name;
941
- if (!fileName) {
942
- return;
943
- }
944
- if (props.onUploadStart) {
945
- props.onUploadStart();
814
+ componentDidCatch(error, errorInfo) {
815
+ console.error('Uncaught error:', error, errorInfo);
816
+ }
817
+ render() {
818
+ if (this.state.error) {
819
+ return (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), title: "Something went wrong", color: "red" }, core.normalizeErrorString(this.state.error)));
946
820
  }
947
- const filename = file.name;
948
- const contentType = file.type || 'application/octet-stream';
949
- medplum
950
- .createBinary(file, filename, contentType, props.onUploadProgress)
951
- .then((binary) => {
952
- props.onUpload({
953
- contentType: binary.contentType,
954
- url: binary.url,
955
- title: filename,
956
- });
957
- })
958
- .catch((outcome) => {
959
- alert(outcome?.issue?.[0]?.details?.text);
960
- });
821
+ return this.props.children;
961
822
  }
962
- return (React.createElement(React.Fragment, null,
963
- React.createElement("input", { type: "file", "data-testid": "upload-file-input", style: { display: 'none' }, ref: fileInputRef, onChange: (e) => onFileChange(e) }),
964
- props.children({ onClick })));
965
823
  }
966
824
 
967
- function AttachmentArrayInput(props) {
968
- const [values, setValues] = React.useState(props.defaultValue ?? []);
969
- const valuesRef = React.useRef();
970
- valuesRef.current = values;
971
- function setValuesWrapper(newValues) {
972
- setValues(newValues);
973
- if (props.onChange) {
974
- props.onChange(newValues);
975
- }
976
- }
977
- return (React.createElement("table", { style: { width: '100%' } },
978
- React.createElement("colgroup", null,
979
- React.createElement("col", { width: "97%" }),
980
- React.createElement("col", { width: "3%" })),
981
- React.createElement("tbody", null,
982
- values.map((v, index) => (React.createElement("tr", { key: `${index}-${values.length}` },
983
- React.createElement("td", null,
984
- React.createElement(AttachmentDisplay, { value: v, maxWidth: 200 })),
985
- React.createElement("td", null,
986
- React.createElement(core$1.ActionIcon, { title: "Remove", size: "sm", onClick: (e) => {
987
- killEvent(e);
988
- const copy = values.slice();
989
- copy.splice(index, 1);
990
- setValuesWrapper(copy);
991
- } },
992
- React.createElement(IconCircleMinus, null)))))),
993
- React.createElement("tr", null,
994
- React.createElement("td", null),
995
- React.createElement("td", null,
996
- React.createElement(AttachmentButton, { onUpload: (attachment) => {
997
- setValuesWrapper([...valuesRef.current, attachment]);
998
- } }, (props) => (React.createElement(core$1.ActionIcon, { ...props, title: "Add", size: "sm", color: "green" },
999
- React.createElement(IconCloudUpload, { size: 16 })))))))));
825
+ function Loading() {
826
+ return (React.createElement(core$1.Center, { style: { width: '100%', height: '100vh' } },
827
+ React.createElement(core$1.Loader, null)));
1000
828
  }
1001
829
 
1002
- function AttachmentInput(props) {
1003
- const [value, setValue] = React.useState(props.defaultValue);
1004
- function setValueWrapper(newValue) {
1005
- setValue(newValue);
1006
- if (props.onChange) {
1007
- props.onChange(newValue);
1008
- }
1009
- }
1010
- if (value) {
1011
- return (React.createElement(React.Fragment, null,
1012
- React.createElement(AttachmentDisplay, { value: value, maxWidth: 200 }),
1013
- React.createElement(core$1.Button, { onClick: (e) => {
1014
- killEvent(e);
1015
- setValueWrapper(undefined);
1016
- } }, "Remove")));
830
+ function HumanNameDisplay(props) {
831
+ const name = props.value;
832
+ if (!name) {
833
+ return null;
1017
834
  }
1018
- return (React.createElement(AttachmentButton, { onUpload: setValueWrapper }, (props) => React.createElement(core$1.Button, { ...props }, "Upload...")));
1019
- }
1020
-
1021
- const useStyles$e = core$1.createStyles(() => ({
1022
- root: {
1023
- '@media (max-width: 800px)': {
1024
- paddingLeft: 4,
1025
- paddingRight: 4,
1026
- },
1027
- },
1028
- }));
1029
- function Container(props) {
1030
- const { children, ...others } = props;
1031
- const { classes } = useStyles$e();
1032
- return (React.createElement(core$1.Container, { className: classes.root, ...others }, children));
1033
- }
1034
-
1035
- const useStyles$d = core$1.createStyles((theme, { width, fill }) => ({
1036
- paper: {
1037
- maxWidth: width,
1038
- margin: `${theme.spacing.xl} auto`,
1039
- padding: fill ? 0 : theme.spacing.md,
1040
- '@media (max-width: 800px)': {
1041
- padding: fill ? 0 : 8,
1042
- },
1043
- '& img': {
1044
- width: '100%',
1045
- maxWidth: '100%',
1046
- },
1047
- '& video': {
1048
- width: '100%',
1049
- maxWidth: '100%',
1050
- },
1051
- },
1052
- }));
1053
- const defaultProps$1 = {
1054
- shadow: 'xs',
1055
- radius: 'md',
1056
- withBorder: true,
1057
- };
1058
- function Panel(props) {
1059
- const { className, children, width, fill, unstyled, ...others } = core$1.useComponentDefaultProps('Panel', defaultProps$1, props);
1060
- const { classes, cx } = useStyles$d({ width, fill }, { name: 'Panel', unstyled });
1061
- return (React.createElement(core$1.Paper, { className: cx(classes.paper, className), ...others }, children));
1062
- }
1063
-
1064
- function Document(props) {
1065
- const { children, ...others } = props;
1066
- return (React.createElement(Container, null,
1067
- React.createElement(Panel, { ...others }, children)));
835
+ return React.createElement(React.Fragment, null, core.formatHumanName(name, props.options));
1068
836
  }
1069
837
 
1070
838
  /**
1071
- * Parses an HTML form and returns the result as a JavaScript object.
1072
- * @param form The HTML form element.
839
+ * Kills a browser event.
840
+ * Prevents default behavior.
841
+ * Stops event propagation.
842
+ * @param e The event.
1073
843
  */
1074
- function parseForm(form) {
1075
- const result = {};
1076
- for (const element of Array.from(form.elements)) {
1077
- if (element instanceof HTMLInputElement) {
1078
- parseInputElement(result, element);
1079
- }
1080
- else if (element instanceof HTMLTextAreaElement) {
1081
- result[element.name] = element.value;
1082
- }
1083
- else if (element instanceof HTMLSelectElement) {
1084
- parseSelectElement(result, element);
1085
- }
1086
- }
1087
- return result;
844
+ function killEvent(e) {
845
+ e.preventDefault();
846
+ e.stopPropagation();
1088
847
  }
1089
848
  /**
1090
- * Parses an HTML input element.
1091
- * Sets the name/value pair in the result,
1092
- * but only if the element is enabled and checked.
1093
- * @param el The input element.
1094
- * @param result The result builder.
849
+ * Returns true if the element is a checkbox or a table cell containing a checkbox.
850
+ * Table cells containing checkboxes are commonly accidentally clicked.
851
+ * @param el The HTML DOM element.
852
+ * @returns True if the element is a checkbox or a table cell containing a checkbox.
1095
853
  */
1096
- function parseInputElement(result, el) {
1097
- if (el.disabled) {
1098
- // Ignore disabled elements
1099
- return;
854
+ function isCheckboxCell(el) {
855
+ if (isCheckboxElement(el)) {
856
+ return true;
1100
857
  }
1101
- if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
1102
- // Ignore unchecked radio or checkbox elements
1103
- return;
858
+ if (el instanceof HTMLTableCellElement) {
859
+ const children = el.children;
860
+ if (children.length === 1 && isCheckboxElement(children[0])) {
861
+ return true;
862
+ }
1104
863
  }
1105
- result[el.name] = el.value;
864
+ return false;
1106
865
  }
1107
- /**
1108
- * Parses an HTML select element.
1109
- * Sets the name/value pair if one is selected.
1110
- * @param result The result builder.
1111
- * @param el The select element.
1112
- */
1113
- function parseSelectElement(result, el) {
1114
- result[el.name] = el.value;
866
+ function isCheckboxElement(el) {
867
+ return el instanceof HTMLInputElement && el.type === 'checkbox';
1115
868
  }
1116
869
 
1117
- function Form(props) {
1118
- return (React.createElement("form", { style: props.style, "data-testid": props.testid, onSubmit: (e) => {
1119
- e.preventDefault();
1120
- const formData = parseForm(e.target);
1121
- if (props.onSubmit) {
1122
- props.onSubmit(formData);
870
+ function MedplumLink(props) {
871
+ const navigate = useMedplumNavigate();
872
+ const { to, suffix, label, onClick, children, ...rest } = props;
873
+ let href = getHref(to);
874
+ if (suffix) {
875
+ href += '/' + suffix;
876
+ }
877
+ return (React.createElement(core$1.Anchor, { href: href, "aria-label": label, onClick: (e) => {
878
+ killEvent(e);
879
+ if (onClick) {
880
+ onClick(e);
1123
881
  }
1124
- } }, props.children));
882
+ else if (to) {
883
+ navigate(href);
884
+ }
885
+ }, ...rest }, children));
1125
886
  }
1126
-
1127
- function Logo(props) {
1128
- return (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 491 491", style: { width: props.size, height: props.size } },
1129
- React.createElement("title", null, "Medplum Logo"),
1130
- React.createElement("path", { fill: props.fill || '#ad7136', d: "M282 67c6-16 16-29 29-40L289 0c-22 17-37 41-43 68l17 23 19-24z" }),
1131
- React.createElement("path", { fill: props.fill || '#946af9', d: "M311 63c-17 0-33 4-48 11-16-7-32-11-49-11-87 0-158 96-158 214s71 214 158 214c17 0 33-4 49-11 15 7 31 11 48 11 87 0 158-96 158-214S398 63 311 63z" }),
1132
- React.createElement("path", { fill: props.fill || '#7857c5', d: "M231 489l-17 2c-87 0-158-96-158-214S127 63 214 63l17 1c-39 12-70 102-70 213s31 201 70 212z" }),
1133
- React.createElement("path", { fill: props.fill || '#40bc26', d: "M207 220a176 176 0 01-177 43A176 176 0 01251 43l1 5c17 59 2 125-45 172z" }),
1134
- React.createElement("path", { fill: props.fill || '#33961e', d: "M252 48A421 421 0 0057 270l-27-7A176 176 0 01251 43l1 5z" })));
887
+ function getHref(to) {
888
+ if (to) {
889
+ if (typeof to === 'string') {
890
+ return getStringHref(to);
891
+ }
892
+ else if (core.isResource(to)) {
893
+ return getResourceHref(to);
894
+ }
895
+ else if (core.isReference(to)) {
896
+ return getReferenceHref(to);
897
+ }
898
+ }
899
+ return '#';
1135
900
  }
1136
-
1137
- function getErrorsForInput(outcome, expression) {
1138
- return outcome?.issue
1139
- ?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression))
1140
- ?.map((issue) => issue.details?.text)
1141
- ?.join('\n');
901
+ function getStringHref(to) {
902
+ if (to.startsWith('http://') || to.startsWith('https://') || to.startsWith('/')) {
903
+ return to;
904
+ }
905
+ return '/' + to;
1142
906
  }
1143
- function getIssuesForExpression(outcome, expression) {
1144
- return outcome?.issue?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression));
907
+ function getResourceHref(to) {
908
+ return `/${to.resourceType}/${to.id}`;
1145
909
  }
1146
- function isExpressionMatch(expr1, expr2) {
1147
- // Expression can be either "fieldName" or "resourceType.fieldName"
1148
- if (expr1 === expr2) {
1149
- return true;
1150
- }
1151
- if (!expr1 || !expr2) {
1152
- return false;
1153
- }
1154
- const dot1 = expr1.indexOf('.');
1155
- if (dot1 >= 0 && expr1.substring(dot1 + 1) === expr2) {
1156
- return true;
1157
- }
1158
- const dot2 = expr2.indexOf('.');
1159
- if (dot2 >= 0 && expr2.substring(dot2 + 1) === expr1) {
1160
- return true;
1161
- }
1162
- return false;
910
+ function getReferenceHref(to) {
911
+ return `/${to.reference}`;
1163
912
  }
1164
913
 
1165
- function NewProjectForm(props) {
914
+ /**
915
+ * React Hook to use a FHIR reference.
916
+ * Handles the complexity of resolving references and caching resources.
917
+ * @param value The resource or reference to resource.
918
+ * @returns The resolved resource.
919
+ */
920
+ function useResource(value, setOutcome) {
1166
921
  const medplum = useMedplum();
1167
- const [outcome, setOutcome] = React.useState();
1168
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: async (formData) => {
1169
- try {
1170
- props.handleAuthResponse(await medplum.startNewProject({
1171
- login: props.login,
1172
- projectName: formData.projectName,
1173
- }));
1174
- }
1175
- catch (err) {
1176
- setOutcome(err);
1177
- }
1178
- } },
1179
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
1180
- React.createElement(Logo, { size: 32 }),
1181
- React.createElement(core$1.Title, null, "Create project")),
1182
- React.createElement(core$1.Stack, { spacing: "xl" },
1183
- React.createElement(core$1.TextInput, { name: "projectName", label: "Project Name", placeholder: "My Project", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'firstName') }),
1184
- React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
1185
- "By clicking submit you agree to the Medplum",
1186
- ' ',
1187
- React.createElement(core$1.Anchor, { href: "https://www.medplum.com/privacy" }, "Privacy\u00A0Policy"),
1188
- ' and ',
1189
- React.createElement(core$1.Anchor, { href: "https://www.medplum.com/terms" }, "Terms\u00A0of\u00A0Service"),
1190
- ".")),
1191
- React.createElement(core$1.Group, { position: "right", mt: "xl", noWrap: true },
1192
- React.createElement(core$1.Button, { type: "submit" }, "Create project"))));
922
+ const [resource, setResource] = React.useState(getInitialResource(medplum, value));
923
+ const setResourceIfChanged = React.useCallback((r) => {
924
+ if (!core.deepEquals(r, resource)) {
925
+ setResource(r);
926
+ }
927
+ }, [resource, setResource]);
928
+ React.useEffect(() => {
929
+ setResourceIfChanged(getInitialResource(medplum, value));
930
+ }, [medplum, value, setResourceIfChanged]);
931
+ React.useEffect(() => {
932
+ let subscribed = true;
933
+ if (core.isReference(value)) {
934
+ medplum
935
+ .readReference(value)
936
+ .then((r) => {
937
+ if (subscribed) {
938
+ setResourceIfChanged(r);
939
+ }
940
+ })
941
+ .catch((err) => {
942
+ if (subscribed) {
943
+ setResourceIfChanged(undefined);
944
+ if (setOutcome) {
945
+ setOutcome(core.normalizeOperationOutcome(err));
946
+ }
947
+ }
948
+ });
949
+ }
950
+ return (() => (subscribed = false));
951
+ }, [medplum, resource, value, setResourceIfChanged, setOutcome]);
952
+ return resource;
1193
953
  }
1194
-
1195
954
  /**
1196
- * Dynamically creates a script tag for the specified JavaScript file.
1197
- * @param src The JavaScript file URL.
955
+ * Returns the initial resource value based on the input value.
956
+ * If the input value is a resource, returns the resource.
957
+ * If the input value is a reference to a resource available in the cache, returns the resource.
958
+ * Otherwise, returns undefined.
959
+ * @param medplum The medplum client.
960
+ * @param value The resource or reference to resource.
961
+ * @returns An initial resource if available; undefined otherwise.
1198
962
  */
1199
- function createScriptTag(src, onload) {
1200
- const head = document.getElementsByTagName('head')[0];
1201
- const script = document.createElement('script');
1202
- script.async = true;
1203
- script.src = src;
1204
- script.onload = onload || null;
1205
- head.appendChild(script);
963
+ function getInitialResource(medplum, value) {
964
+ if (value) {
965
+ if (core.isResource(value)) {
966
+ return value;
967
+ }
968
+ if (core.isReference(value)) {
969
+ return medplum.getCachedReference(value);
970
+ }
971
+ }
972
+ return undefined;
1206
973
  }
1207
974
 
1208
- function GoogleButton(props) {
1209
- const medplum = useMedplum();
1210
- const { googleClientId, handleGoogleCredential } = props;
1211
- const parentRef = React.useRef(null);
1212
- const [scriptLoaded, setScriptLoaded] = React.useState(typeof google !== 'undefined');
1213
- const [initialized, setInitialized] = React.useState(false);
1214
- const [buttonRendered, setButtonRendered] = React.useState(false);
1215
- React.useEffect(() => {
1216
- if (typeof google === 'undefined') {
1217
- createScriptTag('https://accounts.google.com/gsi/client', () => setScriptLoaded(true));
975
+ function ResourceAvatar(props) {
976
+ const resource = useResource(props.value);
977
+ const text = resource ? core.getDisplayString(resource) : props.alt ?? '';
978
+ const imageUrl = (resource && core.getImageSrc(resource)) ?? props.src;
979
+ const radius = props.radius ?? 'xl';
980
+ const avatarProps = { ...props };
981
+ delete avatarProps.value;
982
+ delete avatarProps.link;
983
+ if (props.link) {
984
+ return (React.createElement(MedplumLink, { to: resource },
985
+ React.createElement(core$1.Avatar, { src: imageUrl, alt: text, radius: radius, ...avatarProps })));
986
+ }
987
+ return React.createElement(core$1.Avatar, { src: imageUrl, alt: text, radius: radius, ...avatarProps });
988
+ }
989
+
990
+ function AsyncAutocomplete(props) {
991
+ const { defaultValue, toKey, toOption, loadOptions, onChange, onCreate, creatable, ...rest } = props;
992
+ const defaultItems = toDefaultItems(defaultValue);
993
+ const inputRef = React.useRef(null);
994
+ const [lastValue, setLastValue] = React.useState(undefined);
995
+ const [timer, setTimer] = React.useState();
996
+ const [abortController, setAbortController] = React.useState();
997
+ const [autoSubmit, setAutoSubmit] = React.useState();
998
+ const [options, setOptions] = React.useState(defaultItems?.map(toOption));
999
+ const lastValueRef = React.useRef();
1000
+ lastValueRef.current = lastValue;
1001
+ const timerRef = React.useRef();
1002
+ timerRef.current = timer;
1003
+ const abortControllerRef = React.useRef();
1004
+ abortControllerRef.current = abortController;
1005
+ const autoSubmitRef = React.useRef();
1006
+ autoSubmitRef.current = autoSubmit;
1007
+ const optionsRef = React.useRef();
1008
+ optionsRef.current = options;
1009
+ const handleTimer = React.useCallback(() => {
1010
+ setTimer(undefined);
1011
+ const value = inputRef.current?.value?.trim() || '';
1012
+ if (value === lastValueRef.current) {
1013
+ // Nothing has changed, move on
1218
1014
  return;
1219
1015
  }
1220
- if (!initialized) {
1221
- google.accounts.id.initialize({
1222
- client_id: googleClientId,
1223
- callback: handleGoogleCredential,
1224
- });
1225
- setInitialized(true);
1016
+ setLastValue(value);
1017
+ const newAbortController = new AbortController();
1018
+ setAbortController(newAbortController);
1019
+ loadOptions(value, newAbortController.signal)
1020
+ .then((newValues) => {
1021
+ if (!newAbortController.signal.aborted) {
1022
+ setOptions(newValues.map(toOption));
1023
+ setAbortController(undefined);
1024
+ if (autoSubmitRef.current) {
1025
+ if (newValues.length > 0) {
1026
+ onChange(newValues.slice(0, 1));
1027
+ }
1028
+ setAutoSubmit(false);
1029
+ }
1030
+ }
1031
+ })
1032
+ .catch(console.log);
1033
+ }, [loadOptions, onChange, toOption]);
1034
+ const handleSearchChange = React.useCallback(() => {
1035
+ if (abortControllerRef.current) {
1036
+ abortControllerRef.current.abort();
1037
+ setAbortController(undefined);
1226
1038
  }
1227
- if (parentRef.current && !buttonRendered) {
1228
- google.accounts.id.renderButton(parentRef.current, {});
1229
- setButtonRendered(true);
1039
+ if (timerRef.current !== undefined) {
1040
+ window.clearTimeout(timerRef.current);
1230
1041
  }
1231
- }, [medplum, googleClientId, initialized, scriptLoaded, parentRef, buttonRendered, handleGoogleCredential]);
1232
- if (!googleClientId) {
1233
- return null;
1234
- }
1235
- return React.createElement("div", { ref: parentRef });
1042
+ const newTimer = window.setTimeout(() => handleTimer(), 100);
1043
+ setTimer(newTimer);
1044
+ }, [handleTimer]);
1045
+ const handleChange = React.useCallback((values) => {
1046
+ const result = [];
1047
+ for (const value of values) {
1048
+ let item = optionsRef.current?.find((option) => option.value === value)?.resource;
1049
+ if (!item && creatable !== false) {
1050
+ item = onCreate(value);
1051
+ }
1052
+ if (item)
1053
+ result.push(item);
1054
+ }
1055
+ onChange(result);
1056
+ }, [creatable, onChange, onCreate]);
1057
+ const handleKeyDown = React.useCallback((e) => {
1058
+ if (e.key === 'Enter') {
1059
+ if (!timerRef.current && !abortControllerRef.current) {
1060
+ killEvent(e);
1061
+ if (optionsRef.current && optionsRef.current.length > 0) {
1062
+ setOptions(optionsRef.current.slice(0, 1));
1063
+ handleChange([optionsRef.current[0].value]);
1064
+ }
1065
+ }
1066
+ else {
1067
+ // The user pressed enter, but we don't have results yet.
1068
+ // We need to wait for the results to come in.
1069
+ setAutoSubmit(true);
1070
+ }
1071
+ }
1072
+ }, [handleChange]);
1073
+ const handleCreate = React.useCallback((input) => {
1074
+ const option = toOption(onCreate(input));
1075
+ setOptions([...optionsRef.current, option]);
1076
+ return option;
1077
+ }, [onCreate, setOptions, toOption]);
1078
+ const handleFilter = React.useCallback((_value, selected) => !selected, []);
1079
+ React.useEffect(() => {
1080
+ return () => {
1081
+ if (abortControllerRef.current) {
1082
+ abortControllerRef.current.abort();
1083
+ }
1084
+ };
1085
+ }, []);
1086
+ return (React.createElement(core$1.MultiSelect, { ...rest, ref: inputRef, defaultValue: defaultItems.map(toKey), searchable: true, onKeyDown: handleKeyDown, onSearchChange: handleSearchChange, data: options, onFocus: handleTimer, onChange: handleChange, onCreate: handleCreate, rightSectionWidth: 40, rightSection: abortController ? React.createElement(core$1.Loader, { size: 16 }) : null, filter: handleFilter, creatable: true }));
1236
1087
  }
1237
- function getGoogleClientId(clientId) {
1238
- if (clientId) {
1239
- return clientId;
1088
+ function toDefaultItems(defaultValue) {
1089
+ if (!defaultValue) {
1090
+ return [];
1240
1091
  }
1241
- if (typeof window !== 'undefined') {
1242
- const origin = window.location.protocol + '//' + window.location.host;
1243
- const authorizedOrigins = "http://localhost:3000,http://127.0.0.1:3000,http://localhost:6006,http://127.0.0.1:6006,https://app.medplum.com,https://docs.medplum.com,https://storybook.medplum.com,https://graphiql.medplum.com,https://www.medplum.com"?.split(',') ?? [];
1244
- if (authorizedOrigins.includes(origin)) {
1245
- return "921088377005-3j1sa10vr6hj86jgmdfh2l53v3mp7lfi.apps.googleusercontent.com";
1246
- }
1092
+ if (Array.isArray(defaultValue)) {
1093
+ return defaultValue;
1247
1094
  }
1248
- return undefined;
1095
+ return [defaultValue];
1249
1096
  }
1250
1097
 
1251
- function OperationOutcomeAlert(props) {
1252
- const issues = props.outcome?.issue || props.issues;
1253
- if (!issues) {
1254
- return null;
1098
+ const useStyles$h = core$1.createStyles(() => {
1099
+ return {
1100
+ searchInput: {
1101
+ input: {
1102
+ width: 220,
1103
+ transition: 'width 0.2s',
1104
+ },
1105
+ 'input:focus': {
1106
+ width: 400,
1107
+ },
1108
+ '@media (max-width: 800px)': {
1109
+ input: {
1110
+ width: 150,
1111
+ },
1112
+ 'input:focus': {
1113
+ width: 150,
1114
+ },
1115
+ },
1116
+ },
1117
+ };
1118
+ });
1119
+ function toKey$1(resource) {
1120
+ return resource.id;
1121
+ }
1122
+ function toOption$1(resource) {
1123
+ return {
1124
+ value: resource.id,
1125
+ label: core.getDisplayString(resource),
1126
+ resource,
1127
+ };
1128
+ }
1129
+ function HeaderSearchInput() {
1130
+ const { classes } = useStyles$h();
1131
+ const navigate = useMedplumNavigate();
1132
+ const medplum = useMedplum();
1133
+ const location = reactRouterDom.useLocation();
1134
+ const loadData = React.useCallback(async (input, signal) => {
1135
+ const query = buildGraphQLQuery(input);
1136
+ const options = { signal };
1137
+ const response = (await medplum.graphql(query, undefined, undefined, options));
1138
+ return getResourcesFromResponse(response, input);
1139
+ }, [medplum]);
1140
+ const handleSelect = React.useCallback((item) => {
1141
+ if (item.length > 0) {
1142
+ navigate(`/${core.getReferenceString(item[0])}`);
1143
+ }
1144
+ }, [navigate]);
1145
+ return (React.createElement(AsyncAutocomplete, { key: location.pathname, size: "sm", radius: "md", className: classes.searchInput, icon: React.createElement(IconSearch, { size: 16 }), placeholder: "Search", itemComponent: ItemComponent$1, toKey: toKey$1, toOption: toOption$1, onChange: handleSelect, loadOptions: loadData }));
1146
+ }
1147
+ const ItemComponent$1 = React.forwardRef(({ resource, ...others }, ref) => {
1148
+ let helpText = undefined;
1149
+ if (resource.resourceType === 'Patient') {
1150
+ helpText = resource.birthDate;
1255
1151
  }
1256
- return (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), color: "red" }, issues.map((issue) => (React.createElement("div", { "data-testid": "text-field-error", key: issue.details?.text }, issue.details?.text)))));
1152
+ else if (resource.resourceType === 'ServiceRequest') {
1153
+ helpText = resource.subject?.display;
1154
+ }
1155
+ return (React.createElement("div", { ref: ref, ...others },
1156
+ React.createElement(core$1.Group, { noWrap: true },
1157
+ React.createElement(ResourceAvatar, { value: resource }),
1158
+ React.createElement("div", null,
1159
+ React.createElement(core$1.Text, null, core.getDisplayString(resource)),
1160
+ React.createElement(core$1.Text, { size: "xs", color: "dimmed" }, helpText)))));
1161
+ });
1162
+ function buildGraphQLQuery(input) {
1163
+ const escaped = JSON.stringify(input);
1164
+ if (core.isUUID(input)) {
1165
+ return `{
1166
+ Patients1: PatientList(_id: ${escaped}, _count: 1) {
1167
+ resourceType
1168
+ id
1169
+ identifier {
1170
+ system
1171
+ value
1172
+ }
1173
+ name {
1174
+ given
1175
+ family
1176
+ }
1177
+ birthDate
1178
+ }
1179
+ ServiceRequestList(_id: ${escaped}, _count: 1) {
1180
+ resourceType
1181
+ id
1182
+ identifier {
1183
+ system
1184
+ value
1185
+ }
1186
+ subject {
1187
+ display
1188
+ }
1189
+ }
1190
+ }`.replace(/\s+/g, ' ');
1191
+ }
1192
+ return `{
1193
+ Patients1: PatientList(name: ${escaped}, _count: 5) {
1194
+ resourceType
1195
+ id
1196
+ identifier {
1197
+ system
1198
+ value
1199
+ }
1200
+ name {
1201
+ given
1202
+ family
1203
+ }
1204
+ birthDate
1205
+ }
1206
+ Patients2: PatientList(identifier: ${escaped}, _count: 5) {
1207
+ resourceType
1208
+ id
1209
+ identifier {
1210
+ system
1211
+ value
1212
+ }
1213
+ name {
1214
+ given
1215
+ family
1216
+ }
1217
+ birthDate
1218
+ }
1219
+ ServiceRequestList(identifier: ${escaped}, _count: 5) {
1220
+ resourceType
1221
+ id
1222
+ identifier {
1223
+ system
1224
+ value
1225
+ }
1226
+ subject {
1227
+ display
1228
+ }
1229
+ }
1230
+ }`.replace(/\s+/g, ' ');
1257
1231
  }
1258
-
1259
1232
  /**
1260
- * Dynamically loads the recaptcha script.
1261
- * We do not want to load the script on page load unless the user needs it.
1262
- * @param siteKey The reCAPTCHA site key, available from the reCAPTCHA admin page.
1233
+ * Returns a de-duped and sorted list of resources from the search response.
1234
+ * The search request is actually 3+ separate searches, which can include duplicates.
1235
+ * This function combines the results, de-dupes, and sorts by relevance.
1236
+ * @param response The response from a search query.
1237
+ * @param query The user entered search query.
1238
+ * @returns The resources to display in the autocomplete.
1263
1239
  */
1264
- function initRecaptcha(siteKey) {
1265
- if (typeof grecaptcha === 'undefined') {
1266
- createScriptTag('https://www.google.com/recaptcha/api.js?render=' + siteKey);
1240
+ function getResourcesFromResponse(response, query) {
1241
+ const resources = [];
1242
+ if (response.data.Patients1) {
1243
+ resources.push(...response.data.Patients1);
1244
+ }
1245
+ if (response.data.Patients2) {
1246
+ resources.push(...response.data.Patients2);
1267
1247
  }
1248
+ if (response.data.ServiceRequestList) {
1249
+ resources.push(...response.data.ServiceRequestList);
1250
+ }
1251
+ return sortByRelevance(dedupeResources(resources), query).slice(0, 5);
1268
1252
  }
1269
1253
  /**
1270
- * Starts a request to generate a recapcha token.
1271
- * @param siteKey The reCAPTCHA site key, available from the reCAPTCHA admin page.
1272
- * @returns Promise to a recaptcha token for the current user.
1254
+ * Removes duplicate resources from an array by ID.
1255
+ * @param resources The array of resources with possible duplicates.
1256
+ * @returns The array of resources with no duplicates.
1273
1257
  */
1274
- function getRecaptcha(siteKey) {
1275
- return new Promise((resolve, reject) => {
1276
- grecaptcha.ready(async () => {
1277
- try {
1278
- resolve(await grecaptcha.execute(siteKey, { action: 'submit' }));
1279
- }
1280
- catch (err) {
1281
- reject(err);
1282
- }
1283
- });
1258
+ function dedupeResources(resources) {
1259
+ const ids = new Set();
1260
+ const result = [];
1261
+ for (const resource of resources) {
1262
+ if (!ids.has(resource.id)) {
1263
+ ids.add(resource.id);
1264
+ result.push(resource);
1265
+ }
1266
+ }
1267
+ return result;
1268
+ }
1269
+ /**
1270
+ * Sorts an array of resources by relevance.
1271
+ * @param resources The candidate resources.
1272
+ * @param query The user entered search string.
1273
+ * @returns The sorted array of resources.
1274
+ */
1275
+ function sortByRelevance(resources, query) {
1276
+ return resources.sort((a, b) => {
1277
+ return getResourceScore(b, query) - getResourceScore(a, query);
1284
1278
  });
1285
1279
  }
1286
-
1287
- function NewUserForm(props) {
1288
- const googleClientId = getGoogleClientId(props.googleClientId);
1289
- const recaptchaSiteKey = props.recaptchaSiteKey;
1290
- const medplum = useMedplum();
1291
- const [outcome, setOutcome] = React.useState();
1292
- const issues = getIssuesForExpression(outcome, undefined);
1293
- React.useEffect(() => {
1294
- if (recaptchaSiteKey) {
1295
- initRecaptcha(recaptchaSiteKey);
1280
+ /**
1281
+ * Calculates a relevance score of a candidate resource.
1282
+ * Higher scores are better.
1283
+ * @param resource The candidate resource.
1284
+ * @param query The user entered search string.
1285
+ * @returns The relevance score of the candidate resource.
1286
+ */
1287
+ function getResourceScore(resource, query) {
1288
+ let bestScore = 0;
1289
+ if (resource.identifier) {
1290
+ for (const identifier of resource.identifier) {
1291
+ bestScore = Math.max(bestScore, getStringScore(identifier.value, query));
1296
1292
  }
1297
- }, [recaptchaSiteKey]);
1298
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: async (formData) => {
1299
- try {
1300
- let recaptchaToken = '';
1301
- if (recaptchaSiteKey) {
1302
- recaptchaToken = await getRecaptcha(recaptchaSiteKey);
1303
- }
1304
- props.handleAuthResponse(await medplum.startNewUser({
1305
- projectId: props.projectId,
1306
- firstName: formData.firstName,
1307
- lastName: formData.lastName,
1308
- email: formData.email,
1309
- password: formData.password,
1310
- remember: formData.remember === 'true',
1311
- recaptchaSiteKey,
1312
- recaptchaToken,
1313
- }));
1314
- }
1315
- catch (err) {
1316
- setOutcome(err);
1317
- }
1318
- } },
1319
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, props.children),
1320
- React.createElement(OperationOutcomeAlert, { issues: issues }),
1321
- googleClientId && (React.createElement(React.Fragment, null,
1322
- React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
1323
- React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: async (response) => {
1324
- try {
1325
- props.handleAuthResponse(await medplum.startGoogleLogin({
1326
- googleClientId: response.clientId,
1327
- googleCredential: response.credential,
1328
- createUser: true,
1329
- }));
1330
- }
1331
- catch (err) {
1332
- setOutcome(err);
1333
- }
1334
- } })),
1335
- React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
1336
- React.createElement(core$1.Stack, { spacing: "xl" },
1337
- React.createElement(core$1.TextInput, { name: "firstName", type: "text", label: "First name", placeholder: "First name", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'firstName') }),
1338
- React.createElement(core$1.TextInput, { name: "lastName", type: "text", label: "Last name", placeholder: "Last name", required: true, error: getErrorsForInput(outcome, 'lastName') }),
1339
- React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, error: getErrorsForInput(outcome, 'email') }),
1340
- React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, error: getErrorsForInput(outcome, 'password') }),
1341
- React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
1342
- "By clicking submit you agree to the Medplum",
1343
- ' ',
1344
- React.createElement(core$1.Anchor, { href: "https://www.medplum.com/privacy" }, "Privacy\u00A0Policy"),
1345
- ' and ',
1346
- React.createElement(core$1.Anchor, { href: "https://www.medplum.com/terms" }, "Terms\u00A0of\u00A0Service"),
1347
- "."),
1348
- React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
1349
- "This site is protected by reCAPTCHA and the Google",
1350
- ' ',
1351
- React.createElement(core$1.Anchor, { href: "https://policies.google.com/privacy" }, "Privacy\u00A0Policy"),
1352
- ' and ',
1353
- React.createElement(core$1.Anchor, { href: "https://policies.google.com/terms" }, "Terms\u00A0of\u00A0Service"),
1354
- " apply.")),
1355
- React.createElement(core$1.Group, { position: "apart", mt: "xl", noWrap: true },
1356
- React.createElement(core$1.Checkbox, { name: "remember", label: "Remember me", size: "xs" }),
1357
- React.createElement(core$1.Button, { type: "submit" }, "Create account"))));
1293
+ }
1294
+ if (resource.resourceType === 'Patient' && resource.name) {
1295
+ for (const name of resource.name) {
1296
+ bestScore = Math.max(bestScore, getStringScore(core.formatHumanName(name), query));
1297
+ }
1298
+ }
1299
+ return bestScore;
1300
+ }
1301
+ /**
1302
+ * Calculates a relevance score of a candidate display string.
1303
+ * Higher scores are better.
1304
+ * @param str The candidate display string.
1305
+ * @param query The user entered search string.
1306
+ * @returns The relevance score of the candidate string.
1307
+ */
1308
+ function getStringScore(str, query) {
1309
+ if (!str) {
1310
+ return 0;
1311
+ }
1312
+ const index = str.toLowerCase().indexOf(query.toLowerCase());
1313
+ if (index < 0) {
1314
+ return 0;
1315
+ }
1316
+ return 100 - index;
1358
1317
  }
1359
1318
 
1360
- function RegisterForm(props) {
1361
- const { type, projectId, googleClientId, recaptchaSiteKey, onSuccess } = props;
1319
+ const useStyles$g = core$1.createStyles((theme) => ({
1320
+ logoButton: {
1321
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1322
+ borderRadius: theme.radius.sm,
1323
+ transition: 'background-color 100ms ease',
1324
+ '&:hover': {
1325
+ backgroundColor: theme.fn.lighten(theme.fn.variant({ variant: 'filled', color: theme.primaryColor }).background, 0.8),
1326
+ },
1327
+ },
1328
+ user: {
1329
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1330
+ borderRadius: theme.radius.sm,
1331
+ transition: 'background-color 100ms ease',
1332
+ '&:hover': {
1333
+ backgroundColor: theme.fn.lighten(theme.fn.variant({ variant: 'filled', color: theme.primaryColor }).background, 0.8),
1334
+ },
1335
+ },
1336
+ userName: {
1337
+ fontWeight: 500,
1338
+ lineHeight: 1,
1339
+ marginRight: 3,
1340
+ [theme.fn.smallerThan('xs')]: {
1341
+ display: 'none',
1342
+ },
1343
+ },
1344
+ userActive: {
1345
+ backgroundColor: theme.fn.lighten(theme.fn.variant({ variant: 'filled', color: theme.primaryColor }).background, 0.8),
1346
+ },
1347
+ }));
1348
+ function Header(props) {
1349
+ const context = useMedplumContext();
1350
+ const { medplum, profile, navigate } = context;
1351
+ const logins = medplum.getLogins();
1352
+ const { classes, cx } = useStyles$g();
1353
+ const [userMenuOpened, setUserMenuOpened] = React.useState(false);
1354
+ return (React.createElement(core$1.Header, { height: 60, p: 8, style: { zIndex: 101 } },
1355
+ React.createElement(core$1.Group, { position: "apart" },
1356
+ React.createElement(core$1.Group, { spacing: "xs" },
1357
+ React.createElement(core$1.UnstyledButton, { className: classes.logoButton, onClick: props.navbarToggle }, props.logo),
1358
+ React.createElement(HeaderSearchInput, null)),
1359
+ React.createElement(core$1.Menu, { width: 260, shadow: "xl", position: "bottom-end", transitionProps: { transition: 'pop-top-right' }, opened: userMenuOpened, onClose: () => setUserMenuOpened(false) },
1360
+ React.createElement(core$1.Menu.Target, null,
1361
+ React.createElement(core$1.UnstyledButton, { className: cx(classes.user, { [classes.userActive]: userMenuOpened }), onClick: () => setUserMenuOpened((o) => !o) },
1362
+ React.createElement(core$1.Group, { spacing: 7 },
1363
+ React.createElement(ResourceAvatar, { value: profile, radius: "xl", size: 24 }),
1364
+ React.createElement(core$1.Text, { size: "sm", className: classes.userName }, core.formatHumanName(profile?.name?.[0])),
1365
+ React.createElement(IconChevronDown, { size: 12, stroke: 1.5 })))),
1366
+ React.createElement(core$1.Menu.Dropdown, null,
1367
+ React.createElement(core$1.Stack, { align: "center", p: "xl" },
1368
+ React.createElement(ResourceAvatar, { size: "xl", radius: 100, value: context.profile }),
1369
+ React.createElement(HumanNameDisplay, { value: context.profile?.name?.[0] }),
1370
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, medplum.getActiveLogin()?.project?.display)),
1371
+ logins.length > 1 && React.createElement(core$1.Menu.Divider, null),
1372
+ logins.map((login) => login.profile?.reference !== core.getReferenceString(context.profile) && (React.createElement(core$1.Menu.Item, { key: login.profile?.reference, onClick: () => {
1373
+ medplum
1374
+ .setActiveLogin(login)
1375
+ .then(() => window.location.reload())
1376
+ .catch(console.log);
1377
+ } },
1378
+ React.createElement(core$1.Group, null,
1379
+ React.createElement(core$1.Avatar, { radius: "xl" }),
1380
+ React.createElement("div", { style: { flex: 1 } },
1381
+ React.createElement(core$1.Text, { size: "sm", weight: 500 }, login.profile?.display),
1382
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, login.project?.display)))))),
1383
+ React.createElement(core$1.Menu.Divider, null),
1384
+ React.createElement(core$1.Menu.Item, { icon: React.createElement(IconSwitchHorizontal, { size: 14, stroke: 1.5 }), onClick: () => navigate('/signin') }, "Add another account"),
1385
+ React.createElement(core$1.Menu.Item, { icon: React.createElement(IconSettings, { size: 14, stroke: 1.5 }), onClick: () => navigate(`/${core.getReferenceString(profile)}`) }, "Account settings"),
1386
+ React.createElement(core$1.Menu.Item, { icon: React.createElement(IconLogout, { size: 14, stroke: 1.5 }), onClick: async () => {
1387
+ await medplum.signOut();
1388
+ navigate('/signin');
1389
+ } }, "Sign out"),
1390
+ React.createElement(core$1.Text, { size: "xs", color: "dimmed", align: "center" }, props.version))))));
1391
+ }
1392
+
1393
+ function toKey(element) {
1394
+ return element.code;
1395
+ }
1396
+ function toOption(element) {
1397
+ return {
1398
+ value: element.code,
1399
+ label: getDisplay(element),
1400
+ resource: element,
1401
+ };
1402
+ }
1403
+ function createValue(input) {
1404
+ return {
1405
+ code: input,
1406
+ display: input,
1407
+ };
1408
+ }
1409
+ /**
1410
+ * A low-level component to autocomplete based on a FHIR Valueset.
1411
+ */
1412
+ function ValueSetAutocomplete(props) {
1362
1413
  const medplum = useMedplum();
1363
- const [login, setLogin] = React.useState(undefined);
1364
- const [outcome, setOutcome] = React.useState();
1365
- React.useEffect(() => {
1366
- if (type === 'patient' && login) {
1367
- medplum
1368
- .startNewPatient({ login, projectId: projectId })
1369
- .then((response) => medplum.processCode(response.code))
1370
- .then(() => onSuccess())
1371
- .catch((err) => setOutcome(err));
1372
- }
1373
- }, [medplum, type, projectId, login, onSuccess]);
1374
- function handleAuthResponse(response) {
1375
- if (response.code) {
1376
- medplum
1377
- .processCode(response.code)
1378
- .then(() => onSuccess())
1379
- .catch(console.log);
1414
+ const { elementDefinition, creatable, clearable, ...rest } = props;
1415
+ const loadValues = React.useCallback(async (input, signal) => {
1416
+ const system = elementDefinition.binding?.valueSet;
1417
+ const valueSet = await medplum.searchValueSet(system, input, { signal });
1418
+ const valueSetElements = valueSet.expansion?.contains;
1419
+ const newData = [];
1420
+ for (const valueSetElement of valueSetElements) {
1421
+ if (valueSetElement.code && !newData.some((item) => item.code === valueSetElement.code)) {
1422
+ newData.push(valueSetElement);
1423
+ }
1380
1424
  }
1381
- else if (response.login) {
1382
- setLogin(response.login);
1425
+ return newData;
1426
+ }, [medplum, elementDefinition]);
1427
+ return (React.createElement(AsyncAutocomplete, { ...rest, creatable: creatable ?? true, clearable: clearable ?? true, toKey: toKey, toOption: toOption, loadOptions: loadValues, onCreate: createValue, getCreateLabel: creatable === false ? undefined : (query) => `+ Create ${query}` }));
1428
+ }
1429
+ function getDisplay(item) {
1430
+ return item.display || item.code || '';
1431
+ }
1432
+
1433
+ function CodeInput(props) {
1434
+ const [value, setValue] = React.useState(props.defaultValue);
1435
+ function handleChange(newValues) {
1436
+ const newValue = newValues[0];
1437
+ const newCode = valueSetElementToCode(newValue);
1438
+ setValue(newCode);
1439
+ if (props.onChange) {
1440
+ props.onChange(newCode);
1383
1441
  }
1384
1442
  }
1385
- return (React.createElement(Document, { width: 450 },
1386
- outcome && React.createElement("pre", null, JSON.stringify(outcome, null, 2)),
1387
- !login && (React.createElement(NewUserForm, { projectId: projectId, googleClientId: googleClientId, recaptchaSiteKey: recaptchaSiteKey, handleAuthResponse: handleAuthResponse }, props.children)),
1388
- login && type === 'project' && React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse })));
1443
+ return (React.createElement(ValueSetAutocomplete, { elementDefinition: props.property, name: props.name, placeholder: props.placeholder, defaultValue: codeToValueSetElement(value), onChange: handleChange, creatable: props.creatable, maxSelectedValues: props.maxSelectedValues, clearSearchOnChange: props.clearSearchOnChange, clearable: props.clearable }));
1444
+ }
1445
+ function codeToValueSetElement(code) {
1446
+ return code ? { code } : undefined;
1447
+ }
1448
+ function valueSetElementToCode(element) {
1449
+ return element?.code;
1389
1450
  }
1390
1451
 
1391
- function AuthenticationForm(props) {
1392
- const [email, setEmail] = React.useState();
1393
- if (!email) {
1394
- return React.createElement(EmailForm, { setEmail: setEmail, ...props });
1452
+ const useStyles$f = core$1.createStyles((theme) => {
1453
+ return {
1454
+ menuTitle: {
1455
+ margin: '20px 0 4px 6px',
1456
+ fontSize: '9px',
1457
+ fontWeight: 'normal',
1458
+ textTransform: 'uppercase',
1459
+ letterSpacing: '2px',
1460
+ },
1461
+ link: {
1462
+ ...theme.fn.focusStyles(),
1463
+ display: 'flex',
1464
+ alignItems: 'center',
1465
+ textDecoration: 'none',
1466
+ fontSize: theme.fontSizes.sm,
1467
+ color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[7],
1468
+ padding: `8px 12px`,
1469
+ borderRadius: theme.radius.sm,
1470
+ fontWeight: 500,
1471
+ '&:hover': {
1472
+ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
1473
+ color: theme.colorScheme === 'dark' ? theme.white : theme.black,
1474
+ textDecoration: 'none',
1475
+ [`& svg`]: {
1476
+ color: theme.colorScheme === 'dark' ? theme.white : theme.black,
1477
+ },
1478
+ },
1479
+ '& svg': {
1480
+ color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
1481
+ marginRight: theme.spacing.sm,
1482
+ strokeWidth: 1.5,
1483
+ width: 18,
1484
+ height: 18,
1485
+ },
1486
+ },
1487
+ linkActive: {
1488
+ '&, &:hover': {
1489
+ backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).background,
1490
+ color: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).color,
1491
+ [`& svg`]: {
1492
+ color: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).color,
1493
+ },
1494
+ },
1495
+ },
1496
+ };
1497
+ });
1498
+ function Navbar(props) {
1499
+ const { classes } = useStyles$f();
1500
+ const navigate = useMedplumNavigate();
1501
+ function onLinkClick(e, to) {
1502
+ e.stopPropagation();
1503
+ e.preventDefault();
1504
+ navigate(to);
1505
+ if (window.innerWidth < 768) {
1506
+ props.closeNavbar();
1507
+ }
1395
1508
  }
1396
- else {
1397
- return React.createElement(PasswordForm, { email: email, ...props });
1509
+ function navigateResourceType(resourceType) {
1510
+ if (resourceType) {
1511
+ navigate(`/${resourceType}`);
1512
+ }
1398
1513
  }
1514
+ return (React.createElement(core$1.Navbar, { width: { sm: 250 }, p: "xs" },
1515
+ React.createElement(core$1.Navbar.Section, null,
1516
+ React.createElement(CodeInput, { key: window.location.pathname, name: "resourceType", placeholder: "Resource Type", property: {
1517
+ binding: {
1518
+ valueSet: 'http://hl7.org/fhir/ValueSet/resource-types',
1519
+ },
1520
+ }, onChange: (newValue) => navigateResourceType(newValue), creatable: false, maxSelectedValues: 0, clearSearchOnChange: true, clearable: false })),
1521
+ props.menus && (React.createElement(core$1.Navbar.Section, { grow: true }, props.menus.map((menu) => (React.createElement(React.Fragment, { key: `menu-${menu.title}` },
1522
+ React.createElement(core$1.Text, { className: classes.menuTitle }, menu.title),
1523
+ menu.links?.map((link) => (React.createElement(NavbarLink, { key: link.href, to: link.href, onClick: (e) => onLinkClick(e, link.href) },
1524
+ React.createElement(NavLinkIcon, { to: link.href, icon: link.icon }),
1525
+ React.createElement("span", null, link.label)))))))))));
1526
+ }
1527
+ function NavbarLink(props) {
1528
+ const { classes, cx } = useStyles$f();
1529
+ const location = reactRouterDom.useLocation();
1530
+ const [searchParams] = reactRouterDom.useSearchParams();
1531
+ const toUrl = new URL(props.to, window.location.protocol + '//' + window.location.host);
1532
+ const isActive = location.pathname === toUrl.pathname && matchesParams(searchParams, toUrl);
1533
+ return (React.createElement(MedplumLink, { onClick: props.onClick, to: props.to, className: cx(classes.link, { [classes.linkActive]: isActive }) }, props.children));
1399
1534
  }
1400
- function EmailForm(props) {
1401
- const { setEmail, onRegister, handleAuthResponse, children, ...baseLoginRequest } = props;
1402
- const medplum = useMedplum();
1403
- const googleClientId = !props.disableGoogleAuth && getGoogleClientId(props.googleClientId);
1404
- const isExternalAuth = React.useCallback(async (authMethod) => {
1405
- if (!authMethod.authorizeUrl) {
1535
+ /**
1536
+ * Returns true if the search params match.
1537
+ * @param searchParams The current search params.
1538
+ * @param toUrl The destination URL of the link.
1539
+ * @returns True if the search params match.
1540
+ */
1541
+ function matchesParams(searchParams, toUrl) {
1542
+ for (const [key, value] of toUrl.searchParams.entries()) {
1543
+ if (searchParams.get(key) !== value) {
1406
1544
  return false;
1407
1545
  }
1408
- const state = JSON.stringify({
1409
- ...(await medplum.ensureCodeChallenge(baseLoginRequest)),
1410
- domain: authMethod.domain,
1411
- });
1412
- const url = new URL(authMethod.authorizeUrl);
1413
- url.searchParams.set('state', state);
1414
- window.location.assign(url.toString());
1415
- return true;
1416
- }, [medplum, baseLoginRequest]);
1417
- const handleSubmit = React.useCallback(async (formData) => {
1418
- const authMethod = await medplum.post('auth/method', { email: formData.email });
1419
- if (!(await isExternalAuth(authMethod))) {
1420
- setEmail(formData.email);
1421
- }
1422
- }, [medplum, isExternalAuth, setEmail]);
1423
- const handleGoogleCredential = React.useCallback(async (response) => {
1424
- const authResponse = await medplum.startGoogleLogin({
1425
- ...baseLoginRequest,
1426
- googleCredential: response.credential,
1427
- });
1428
- if (!(await isExternalAuth(authResponse))) {
1429
- handleAuthResponse(authResponse);
1430
- }
1431
- }, [medplum, baseLoginRequest, isExternalAuth, handleAuthResponse]);
1432
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
1433
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
1434
- googleClientId && (React.createElement(React.Fragment, null,
1435
- React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
1436
- React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: handleGoogleCredential })),
1437
- React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
1438
- React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, autoFocus: true }),
1439
- React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
1440
- React.createElement("div", null, onRegister && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onRegister, size: "xs" }, "Register"))),
1441
- React.createElement(core$1.Button, { type: "submit" }, "Next"))));
1546
+ }
1547
+ return true;
1442
1548
  }
1443
- function PasswordForm(props) {
1444
- const { onForgotPassword, handleAuthResponse, children, ...baseLoginRequest } = props;
1445
- const medplum = useMedplum();
1446
- const [outcome, setOutcome] = React.useState();
1447
- const issues = getIssuesForExpression(outcome, undefined);
1448
- const handleSubmit = React.useCallback((formData) => {
1449
- medplum
1450
- .startLogin({
1451
- ...baseLoginRequest,
1452
- password: formData.password,
1453
- remember: formData.remember === 'on',
1454
- })
1455
- .then(handleAuthResponse)
1456
- .catch((err) => setOutcome(core.normalizeOperationOutcome(err)));
1457
- }, [medplum, baseLoginRequest, handleAuthResponse]);
1458
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
1459
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
1460
- React.createElement(OperationOutcomeAlert, { issues: issues }),
1461
- React.createElement(core$1.Stack, { spacing: "xl" },
1462
- React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, error: getErrorsForInput(outcome, 'password') })),
1463
- React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
1464
- onForgotPassword && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onForgotPassword, size: "xs" }, "Forgot password")),
1465
- React.createElement(core$1.Checkbox, { id: "remember", name: "remember", label: "Remember me", size: "xs", sx: { lineHeight: 1 } }),
1466
- React.createElement(core$1.Button, { type: "submit" }, "Sign in"))));
1549
+ function NavLinkIcon(props) {
1550
+ if (props.icon) {
1551
+ return props.icon;
1552
+ }
1553
+ return React.createElement(core$1.Space, { w: 30 });
1467
1554
  }
1468
1555
 
1469
- function ChooseProfileForm(props) {
1556
+ function AppShell(props) {
1557
+ const theme = core$1.useMantineTheme();
1558
+ const [navbarOpen, setNavbarOpen] = React.useState(localStorage['navbarOpen'] === 'true');
1470
1559
  const medplum = useMedplum();
1471
- const [outcome, setOutcome] = React.useState();
1472
- return (React.createElement(core$1.Stack, null,
1473
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
1474
- React.createElement(Logo, { size: 32 }),
1475
- React.createElement(core$1.Title, { order: 3 }, "Choose profile")),
1476
- React.createElement(OperationOutcomeAlert, { outcome: outcome }),
1477
- props.memberships.map((membership) => (React.createElement(core$1.UnstyledButton, { key: membership.id, onClick: () => {
1478
- medplum
1479
- .post('auth/profile', {
1480
- login: props.login,
1481
- profile: membership.id,
1482
- })
1483
- .then(props.handleAuthResponse)
1484
- .catch((err) => setOutcome(core.normalizeOperationOutcome(err)));
1485
- } },
1486
- React.createElement(core$1.Group, null,
1487
- React.createElement(core$1.Avatar, { radius: "xl" }),
1488
- React.createElement("div", { style: { flex: 1 } },
1489
- React.createElement(core$1.Text, { size: "sm", weight: 500 }, membership.profile?.display),
1490
- React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, membership.project?.display))))))));
1560
+ const profile = useMedplumProfile();
1561
+ function setNavbarOpenWrapper(open) {
1562
+ localStorage['navbarOpen'] = open.toString();
1563
+ setNavbarOpen(open);
1564
+ }
1565
+ function closeNavbar() {
1566
+ setNavbarOpenWrapper(false);
1567
+ }
1568
+ function toggleNavbar() {
1569
+ setNavbarOpenWrapper(!navbarOpen);
1570
+ }
1571
+ if (medplum.isLoading()) {
1572
+ return React.createElement(Loading, null);
1573
+ }
1574
+ return (React.createElement(core$1.AppShell, { styles: {
1575
+ main: {
1576
+ background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
1577
+ },
1578
+ }, padding: 0, fixed: true, header: profile && React.createElement(Header, { logo: props.logo, version: props.version, navbarToggle: toggleNavbar }), navbar: profile && navbarOpen ? React.createElement(Navbar, { menus: props.menus, closeNavbar: closeNavbar }) : undefined },
1579
+ React.createElement(ErrorBoundary, null,
1580
+ React.createElement(React.Suspense, { fallback: React.createElement(Loading, null) }, props.children))));
1491
1581
  }
1492
1582
 
1493
- function ChooseScopeForm(props) {
1494
- const medplum = useMedplum();
1495
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
1496
- medplum
1497
- .post('auth/scope', {
1498
- login: props.login,
1499
- scope: Object.keys(formData).join(' '),
1500
- })
1501
- .then(props.handleAuthResponse)
1502
- .catch(console.log);
1503
- } },
1504
- React.createElement(core$1.Stack, null,
1505
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
1506
- React.createElement(Logo, { size: 32 }),
1507
- React.createElement(core$1.Title, null, "Choose scope")),
1508
- React.createElement(core$1.Stack, null, (props.scope || 'openid').split(' ').map((scopeName) => (React.createElement(core$1.Checkbox, { key: scopeName, id: scopeName, name: scopeName, label: scopeName, defaultChecked: true })))),
1509
- React.createElement(core$1.Group, { position: "right", mt: "xl" },
1510
- React.createElement(core$1.Button, { type: "submit" }, "Set scope")))));
1583
+ function AttachmentDisplay(props) {
1584
+ const value = props.value;
1585
+ const { contentType, url, title } = value ?? {};
1586
+ if (!url) {
1587
+ return null;
1588
+ }
1589
+ return (React.createElement("div", { "data-testid": "attachment-display" },
1590
+ contentType?.startsWith('image/') && (React.createElement("img", { "data-testid": "attachment-image", style: { maxWidth: props.maxWidth }, src: url, alt: value?.title })),
1591
+ contentType?.startsWith('video/') && (React.createElement("video", { "data-testid": "attachment-video", style: { maxWidth: props.maxWidth }, controls: true },
1592
+ React.createElement("source", { type: contentType, src: url }))),
1593
+ contentType === 'application/pdf' && !title?.endsWith('.pdf') && (React.createElement("div", { "data-testid": "attachment-pdf", style: { maxWidth: props.maxWidth, minHeight: 400 } },
1594
+ React.createElement("iframe", { width: "100%", height: "400", src: url + '#navpanes=0', allowFullScreen: true, frameBorder: 0, seamless: true }))),
1595
+ React.createElement("div", { "data-testid": "download-link", style: { padding: '2px 16px 16px 16px' } },
1596
+ React.createElement(core$1.Anchor, { href: value?.url, "data-testid": "attachment-details", target: "_blank", rel: "noopener noreferrer" }, value?.title || 'Download'))));
1511
1597
  }
1512
1598
 
1513
- function MfaForm(props) {
1514
- const medplum = useMedplum();
1515
- const [errorMessage, setErrorMessage] = React.useState(undefined);
1516
- return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
1517
- setErrorMessage(undefined);
1518
- medplum
1519
- .post('auth/mfa/verify', {
1520
- login: props.login,
1521
- token: formData.token,
1522
- })
1523
- .then(props.handleAuthResponse)
1524
- .catch((err) => setErrorMessage(core.normalizeErrorString(err)));
1525
- } },
1526
- React.createElement(core$1.Stack, null,
1527
- React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
1528
- React.createElement(Logo, { size: 32 }),
1529
- React.createElement(core$1.Title, null, "Enter MFA code")),
1530
- errorMessage && (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), title: "Error", color: "red" }, errorMessage)),
1531
- React.createElement(core$1.Stack, null,
1532
- React.createElement(core$1.TextInput, { name: "token", label: "MFA code", required: true })),
1533
- React.createElement(core$1.Group, { position: "right", mt: "xl" },
1534
- React.createElement(core$1.Button, { type: "submit" }, "Submit code")))));
1599
+ function AttachmentArrayDisplay(props) {
1600
+ return (React.createElement("div", null, props.values &&
1601
+ props.values.map((v, index) => (React.createElement("div", { key: 'attatchment-' + index },
1602
+ React.createElement(AttachmentDisplay, { value: v, maxWidth: props.maxWidth }))))));
1535
1603
  }
1536
1604
 
1537
- /**
1538
- * The SignInForm component allows users to sign in to Medplum.
1539
- *
1540
- * "Signing in" is a multi-step process:
1541
- * 1) Authentication - identify the user
1542
- * 2) MFA - If MFA is enabled, prompt for MFA code
1543
- * 3) Choose profile - If the user has multiple profiles, prompt to choose one
1544
- * 4) Choose scope - If the user has multiple scopes, prompt to choose one
1545
- * 5) Success - Return to the caller with either a code or a redirect
1546
- */
1547
- function SignInForm(props) {
1548
- const { chooseScopes, onSuccess, onForgotPassword, onRegister, onCode, ...baseLoginRequest } = props;
1605
+ function AttachmentButton(props) {
1549
1606
  const medplum = useMedplum();
1550
- const [login, setLogin] = React.useState(undefined);
1551
- const [mfaRequired, setAuthenticatorRequired] = React.useState(false);
1552
- const [memberships, setMemberships] = React.useState(undefined);
1553
- const handleCode = React.useCallback((code) => {
1554
- if (onCode) {
1555
- onCode(code);
1556
- }
1557
- else {
1558
- medplum
1559
- .processCode(code)
1560
- .then(() => {
1561
- if (onSuccess) {
1562
- onSuccess();
1563
- }
1564
- })
1565
- .catch(console.log);
1566
- }
1567
- }, [medplum, onCode, onSuccess]);
1568
- const handleAuthResponse = React.useCallback((response) => {
1569
- setAuthenticatorRequired(!!response.mfaRequired);
1570
- if (response.login) {
1571
- setLogin(response.login);
1572
- }
1573
- if (response.memberships) {
1574
- setMemberships(response.memberships);
1575
- }
1576
- if (response.code) {
1577
- if (chooseScopes) {
1578
- setMemberships(undefined);
1579
- }
1580
- else {
1581
- handleCode(response.code);
1582
- }
1583
- }
1584
- }, [chooseScopes, handleCode]);
1585
- const handleScopeResponse = React.useCallback((response) => {
1586
- handleCode(response.code);
1587
- }, [handleCode]);
1588
- React.useEffect(() => {
1589
- if (props.login) {
1590
- medplum
1591
- .get('auth/login/' + props.login)
1592
- .then(handleAuthResponse)
1593
- .catch(console.error);
1594
- }
1595
- }, [medplum, props, handleAuthResponse]);
1596
- return (React.createElement(Document, { width: 450 }, (() => {
1597
- if (!login) {
1598
- return (React.createElement(AuthenticationForm, { onForgotPassword: onForgotPassword, onRegister: onRegister, handleAuthResponse: handleAuthResponse, disableGoogleAuth: props.disableGoogleAuth, ...baseLoginRequest }, props.children));
1599
- }
1600
- else if (mfaRequired) {
1601
- return React.createElement(MfaForm, { login: login, handleAuthResponse: handleAuthResponse });
1602
- }
1603
- else if (memberships) {
1604
- return React.createElement(ChooseProfileForm, { login: login, memberships: memberships, handleAuthResponse: handleAuthResponse });
1607
+ const fileInputRef = React.useRef(null);
1608
+ function onClick(e) {
1609
+ killEvent(e);
1610
+ fileInputRef.current?.click();
1611
+ }
1612
+ function onFileChange(e) {
1613
+ killEvent(e);
1614
+ const files = e.target.files;
1615
+ if (files) {
1616
+ Array.from(files).forEach(processFile);
1605
1617
  }
1606
- else if (props.projectId === 'new') {
1607
- return React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse });
1618
+ }
1619
+ /**
1620
+ * Processes a single file.
1621
+ *
1622
+ * @param {File} file The file descriptor.
1623
+ */
1624
+ function processFile(file) {
1625
+ if (!file) {
1626
+ return;
1608
1627
  }
1609
- else if (props.chooseScopes) {
1610
- return React.createElement(ChooseScopeForm, { login: login, scope: props.scope, handleAuthResponse: handleScopeResponse });
1628
+ const fileName = file.name;
1629
+ if (!fileName) {
1630
+ return;
1611
1631
  }
1612
- else {
1613
- return React.createElement("div", null, "Success");
1632
+ if (props.onUploadStart) {
1633
+ props.onUploadStart();
1614
1634
  }
1615
- })()));
1635
+ const filename = file.name;
1636
+ const contentType = file.type || 'application/octet-stream';
1637
+ medplum
1638
+ .createBinary(file, filename, contentType, props.onUploadProgress)
1639
+ .then((binary) => {
1640
+ props.onUpload({
1641
+ contentType: binary.contentType,
1642
+ url: binary.url,
1643
+ title: filename,
1644
+ });
1645
+ })
1646
+ .catch((outcome) => {
1647
+ alert(outcome?.issue?.[0]?.details?.text);
1648
+ });
1649
+ }
1650
+ return (React.createElement(React.Fragment, null,
1651
+ React.createElement("input", { type: "file", "data-testid": "upload-file-input", style: { display: 'none' }, ref: fileInputRef, onChange: (e) => onFileChange(e) }),
1652
+ props.children({ onClick })));
1616
1653
  }
1617
1654
 
1618
- const DEFAULT_IGNORED_PROPERTIES = [
1619
- 'meta',
1620
- 'implicitRules',
1621
- 'language',
1622
- 'text',
1623
- 'contained',
1624
- 'extension',
1625
- 'modifierExtension',
1626
- ];
1627
-
1628
- const useStyles$c = core$1.createStyles((theme) => ({
1629
- root: {
1630
- display: 'grid',
1631
- gridTemplateColumns: '30% 70%',
1632
- margin: 0,
1633
- '& > dt, & > dd': {
1634
- padding: `${theme.spacing.sm} ${theme.spacing.sm}`,
1635
- borderTop: `0.1px solid ${theme.colors.gray[3]}`,
1636
- margin: 0,
1637
- },
1638
- },
1639
- compact: {
1640
- gridTemplateColumns: '20% 80%',
1655
+ function AttachmentArrayInput(props) {
1656
+ const [values, setValues] = React.useState(props.defaultValue ?? []);
1657
+ const valuesRef = React.useRef();
1658
+ valuesRef.current = values;
1659
+ function setValuesWrapper(newValues) {
1660
+ setValues(newValues);
1661
+ if (props.onChange) {
1662
+ props.onChange(newValues);
1663
+ }
1664
+ }
1665
+ return (React.createElement("table", { style: { width: '100%' } },
1666
+ React.createElement("colgroup", null,
1667
+ React.createElement("col", { width: "97%" }),
1668
+ React.createElement("col", { width: "3%" })),
1669
+ React.createElement("tbody", null,
1670
+ values.map((v, index) => (React.createElement("tr", { key: `${index}-${values.length}` },
1671
+ React.createElement("td", null,
1672
+ React.createElement(AttachmentDisplay, { value: v, maxWidth: 200 })),
1673
+ React.createElement("td", null,
1674
+ React.createElement(core$1.ActionIcon, { title: "Remove", size: "sm", onClick: (e) => {
1675
+ killEvent(e);
1676
+ const copy = values.slice();
1677
+ copy.splice(index, 1);
1678
+ setValuesWrapper(copy);
1679
+ } },
1680
+ React.createElement(IconCircleMinus, null)))))),
1681
+ React.createElement("tr", null,
1682
+ React.createElement("td", null),
1683
+ React.createElement("td", null,
1684
+ React.createElement(AttachmentButton, { onUpload: (attachment) => {
1685
+ setValuesWrapper([...valuesRef.current, attachment]);
1686
+ } }, (props) => (React.createElement(core$1.ActionIcon, { ...props, title: "Add", size: "sm", color: "green" },
1687
+ React.createElement(IconCloudUpload, { size: 16 })))))))));
1688
+ }
1689
+
1690
+ function AttachmentInput(props) {
1691
+ const [value, setValue] = React.useState(props.defaultValue);
1692
+ function setValueWrapper(newValue) {
1693
+ setValue(newValue);
1694
+ if (props.onChange) {
1695
+ props.onChange(newValue);
1696
+ }
1697
+ }
1698
+ if (value) {
1699
+ return (React.createElement(React.Fragment, null,
1700
+ React.createElement(AttachmentDisplay, { value: value, maxWidth: 200 }),
1701
+ React.createElement(core$1.Button, { onClick: (e) => {
1702
+ killEvent(e);
1703
+ setValueWrapper(undefined);
1704
+ } }, "Remove")));
1705
+ }
1706
+ return (React.createElement(AttachmentButton, { onUpload: setValueWrapper }, (props) => React.createElement(core$1.Button, { ...props }, "Upload...")));
1707
+ }
1708
+
1709
+ const DEFAULT_IGNORED_PROPERTIES = [
1710
+ 'meta',
1711
+ 'implicitRules',
1712
+ 'language',
1713
+ 'text',
1714
+ 'contained',
1715
+ 'extension',
1716
+ 'modifierExtension',
1717
+ ];
1718
+
1719
+ const useStyles$e = core$1.createStyles((theme) => ({
1720
+ root: {
1721
+ display: 'grid',
1722
+ gridTemplateColumns: '30% 70%',
1723
+ margin: 0,
1724
+ '& > dt, & > dd': {
1725
+ padding: `${theme.spacing.sm} ${theme.spacing.sm}`,
1726
+ borderTop: `0.1px solid ${theme.colors.gray[3]}`,
1727
+ margin: 0,
1728
+ },
1729
+ },
1730
+ compact: {
1731
+ gridTemplateColumns: '20% 80%',
1641
1732
  '& > dt, & > dd': {
1642
1733
  padding: `0 ${theme.spacing.xs} ${theme.spacing.xs} 0`,
1643
1734
  border: 0,
@@ -1646,7 +1737,7 @@
1646
1737
  }));
1647
1738
  function DescriptionList(props) {
1648
1739
  const { children, compact } = props;
1649
- const { classes, cx } = useStyles$c();
1740
+ const { classes, cx } = useStyles$e();
1650
1741
  return React.createElement("dl", { className: cx(classes.root, { [classes.compact]: compact }) }, children);
1651
1742
  }
1652
1743
  function DescriptionListEntry(props) {
@@ -1699,14 +1790,6 @@
1699
1790
  contactDetail.telecom?.map((telecom, index) => (React.createElement(ContactPointDisplay, { key: 'telecom-' + index, value: telecom })))));
1700
1791
  }
1701
1792
 
1702
- function HumanNameDisplay(props) {
1703
- const name = props.value;
1704
- if (!name) {
1705
- return null;
1706
- }
1707
- return React.createElement(React.Fragment, null, core.formatHumanName(name, props.options));
1708
- }
1709
-
1710
1793
  function IdentifierDisplay(props) {
1711
1794
  return (React.createElement("div", null,
1712
1795
  props.value?.system,
@@ -1737,50 +1820,6 @@
1737
1820
  React.createElement(QuantityDisplay, { value: value.denominator })));
1738
1821
  }
1739
1822
 
1740
- function MedplumLink(props) {
1741
- const navigate = useMedplumNavigate();
1742
- const { to, suffix, label, onClick, children, ...rest } = props;
1743
- let href = getHref(to);
1744
- if (suffix) {
1745
- href += '/' + suffix;
1746
- }
1747
- return (React.createElement(core$1.Anchor, { href: href, "aria-label": label, onClick: (e) => {
1748
- killEvent(e);
1749
- if (onClick) {
1750
- onClick();
1751
- }
1752
- else if (to) {
1753
- navigate(href);
1754
- }
1755
- }, ...rest }, children));
1756
- }
1757
- function getHref(to) {
1758
- if (to) {
1759
- if (typeof to === 'string') {
1760
- return getStringHref(to);
1761
- }
1762
- else if (core.isResource(to)) {
1763
- return getResourceHref(to);
1764
- }
1765
- else if (core.isReference(to)) {
1766
- return getReferenceHref(to);
1767
- }
1768
- }
1769
- return '#';
1770
- }
1771
- function getStringHref(to) {
1772
- if (to.startsWith('http://') || to.startsWith('https://') || to.startsWith('/')) {
1773
- return to;
1774
- }
1775
- return '/' + to;
1776
- }
1777
- function getResourceHref(to) {
1778
- return `/${to.resourceType}/${to.id}`;
1779
- }
1780
- function getReferenceHref(to) {
1781
- return `/${to.reference}`;
1782
- }
1783
-
1784
1823
  function ReferenceDisplay(props) {
1785
1824
  if (!props.value) {
1786
1825
  return null;
@@ -1947,69 +1986,36 @@
1947
1986
  React.createElement(core$1.Input.Wrapper, { id: props.htmlFor, label: props.title, description: props.description, withAsterisk: props.withAsterisk }, (() => null)()))));
1948
1987
  }
1949
1988
 
1950
- function FormSection(props) {
1951
- return (React.createElement(core$1.Input.Wrapper, { id: props.htmlFor, label: props.title, description: props.description, withAsterisk: props.withAsterisk, error: getErrorsForInput(props.outcome, props.htmlFor) }, props.children));
1989
+ function getErrorsForInput(outcome, expression) {
1990
+ return outcome?.issue
1991
+ ?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression))
1992
+ ?.map((issue) => issue.details?.text)
1993
+ ?.join('\n');
1952
1994
  }
1953
-
1954
- /**
1955
- * React Hook to use a FHIR reference.
1956
- * Handles the complexity of resolving references and caching resources.
1957
- * @param value The resource or reference to resource.
1958
- * @returns The resolved resource.
1959
- */
1960
- function useResource(value, setOutcome) {
1961
- const medplum = useMedplum();
1962
- const [resource, setResource] = React.useState(getInitialResource(medplum, value));
1963
- const setResourceIfChanged = React.useCallback((r) => {
1964
- if (!core.deepEquals(r, resource)) {
1965
- setResource(r);
1966
- }
1967
- }, [resource, setResource]);
1968
- React.useEffect(() => {
1969
- setResourceIfChanged(getInitialResource(medplum, value));
1970
- }, [medplum, value, setResourceIfChanged]);
1971
- React.useEffect(() => {
1972
- let subscribed = true;
1973
- if (core.isReference(value)) {
1974
- medplum
1975
- .readReference(value)
1976
- .then((r) => {
1977
- if (subscribed) {
1978
- setResourceIfChanged(r);
1979
- }
1980
- })
1981
- .catch((err) => {
1982
- if (subscribed) {
1983
- setResourceIfChanged(undefined);
1984
- if (setOutcome) {
1985
- setOutcome(core.normalizeOperationOutcome(err));
1986
- }
1987
- }
1988
- });
1989
- }
1990
- return (() => (subscribed = false));
1991
- }, [medplum, resource, value, setResourceIfChanged, setOutcome]);
1992
- return resource;
1995
+ function getIssuesForExpression(outcome, expression) {
1996
+ return outcome?.issue?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression));
1993
1997
  }
1994
- /**
1995
- * Returns the initial resource value based on the input value.
1996
- * If the input value is a resource, returns the resource.
1997
- * If the input value is a reference to a resource available in the cache, returns the resource.
1998
- * Otherwise, returns undefined.
1999
- * @param medplum The medplum client.
2000
- * @param value The resource or reference to resource.
2001
- * @returns An initial resource if available; undefined otherwise.
2002
- */
2003
- function getInitialResource(medplum, value) {
2004
- if (value) {
2005
- if (core.isResource(value)) {
2006
- return value;
2007
- }
2008
- if (core.isReference(value)) {
2009
- return medplum.getCachedReference(value);
2010
- }
1998
+ function isExpressionMatch(expr1, expr2) {
1999
+ // Expression can be either "fieldName" or "resourceType.fieldName"
2000
+ if (expr1 === expr2) {
2001
+ return true;
2011
2002
  }
2012
- return undefined;
2003
+ if (!expr1 || !expr2) {
2004
+ return false;
2005
+ }
2006
+ const dot1 = expr1.indexOf('.');
2007
+ if (dot1 >= 0 && expr1.substring(dot1 + 1) === expr2) {
2008
+ return true;
2009
+ }
2010
+ const dot2 = expr2.indexOf('.');
2011
+ if (dot2 >= 0 && expr2.substring(dot2 + 1) === expr1) {
2012
+ return true;
2013
+ }
2014
+ return false;
2015
+ }
2016
+
2017
+ function FormSection(props) {
2018
+ return (React.createElement(core$1.Input.Wrapper, { id: props.htmlFor, label: props.title, description: props.description, withAsterisk: props.withAsterisk, error: getErrorsForInput(props.outcome, props.htmlFor) }, props.children));
2013
2019
  }
2014
2020
 
2015
2021
  function ResourceForm(props) {
@@ -2058,46 +2064,6 @@
2058
2064
  return obj;
2059
2065
  }
2060
2066
 
2061
- function toKey(element) {
2062
- return element.code;
2063
- }
2064
- function toOption(element) {
2065
- return {
2066
- value: element.code,
2067
- label: getDisplay(element),
2068
- resource: element,
2069
- };
2070
- }
2071
- function createValue(input) {
2072
- return {
2073
- code: input,
2074
- display: input,
2075
- };
2076
- }
2077
- /**
2078
- * A low-level component to autocomplete based on a FHIR Valueset.
2079
- */
2080
- function ValueSetAutocomplete(props) {
2081
- const medplum = useMedplum();
2082
- const { elementDefinition, creatable, clearable, ...rest } = props;
2083
- const loadValues = React.useCallback(async (input, signal) => {
2084
- const system = elementDefinition.binding?.valueSet;
2085
- const valueSet = await medplum.searchValueSet(system, input, { signal });
2086
- const valueSetElements = valueSet.expansion?.contains;
2087
- const newData = [];
2088
- for (const valueSetElement of valueSetElements) {
2089
- if (valueSetElement.code && !newData.some((item) => item.code === valueSetElement.code)) {
2090
- newData.push(valueSetElement);
2091
- }
2092
- }
2093
- return newData;
2094
- }, [medplum, elementDefinition]);
2095
- return (React.createElement(AsyncAutocomplete, { ...rest, creatable: creatable ?? true, clearable: clearable ?? true, toKey: toKey, toOption: toOption, loadOptions: loadValues, onCreate: createValue, getCreateLabel: creatable === false ? undefined : (query) => `+ Create ${query}` }));
2096
- }
2097
- function getDisplay(item) {
2098
- return item.display || item.code || '';
2099
- }
2100
-
2101
2067
  function CodeableConceptInput(props) {
2102
2068
  const [value, setValue] = React.useState(props.defaultValue);
2103
2069
  function handleChange(newValues) {
@@ -2129,26 +2095,7 @@
2129
2095
  };
2130
2096
  }
2131
2097
 
2132
- function CodeInput(props) {
2133
- const [value, setValue] = React.useState(props.defaultValue);
2134
- function handleChange(newValues) {
2135
- const newValue = newValues[0];
2136
- const newCode = valueSetElementToCode(newValue);
2137
- setValue(newCode);
2138
- if (props.onChange) {
2139
- props.onChange(newCode);
2140
- }
2141
- }
2142
- return (React.createElement(ValueSetAutocomplete, { elementDefinition: props.property, name: props.name, placeholder: props.placeholder, defaultValue: codeToValueSetElement(value), onChange: handleChange, creatable: props.creatable, maxSelectedValues: props.maxSelectedValues, clearSearchOnChange: props.clearSearchOnChange, clearable: props.clearable }));
2143
- }
2144
- function codeToValueSetElement(code) {
2145
- return code ? { code } : undefined;
2146
- }
2147
- function valueSetElementToCode(element) {
2148
- return element?.code;
2149
- }
2150
-
2151
- function CodingInput(props) {
2098
+ function CodingInput(props) {
2152
2099
  const [value, setValue] = React.useState(props.defaultValue);
2153
2100
  function handleChange(newValues) {
2154
2101
  const newValue = newValues[0];
@@ -2518,21 +2465,6 @@
2518
2465
  }) })));
2519
2466
  }
2520
2467
 
2521
- function ResourceAvatar(props) {
2522
- const resource = useResource(props.value);
2523
- const text = resource ? core.getDisplayString(resource) : props.alt ?? '';
2524
- const imageUrl = (resource && core.getImageSrc(resource)) ?? props.src;
2525
- const radius = props.radius ?? 'xl';
2526
- const avatarProps = { ...props };
2527
- delete avatarProps.value;
2528
- delete avatarProps.link;
2529
- if (props.link) {
2530
- return (React.createElement(MedplumLink, { to: resource },
2531
- React.createElement(core$1.Avatar, { src: imageUrl, alt: text, radius: radius, ...avatarProps })));
2532
- }
2533
- return React.createElement(core$1.Avatar, { src: imageUrl, alt: text, radius: radius, ...avatarProps });
2534
- }
2535
-
2536
2468
  /**
2537
2469
  * Defines which search parameters will be used by the type ahead to search for each resourceType
2538
2470
  */
@@ -2924,7 +2856,7 @@
2924
2856
  })));
2925
2857
  }
2926
2858
 
2927
- const useStyles$b = core$1.createStyles((theme) => ({
2859
+ const useStyles$d = core$1.createStyles((theme) => ({
2928
2860
  table: {
2929
2861
  width: 350,
2930
2862
  '& th': {
@@ -2969,7 +2901,7 @@
2969
2901
  return date.toLocaleString('default', { month: 'long' }) + ' ' + date.getFullYear();
2970
2902
  }
2971
2903
  function CalendarInput(props) {
2972
- const { classes } = useStyles$b();
2904
+ const { classes } = useStyles$d();
2973
2905
  const { onChangeMonth, onClick } = props;
2974
2906
  const [month, setMonth] = React.useState(getStartMonth);
2975
2907
  function moveMonth(delta) {
@@ -3052,13 +2984,27 @@
3052
2984
  return false;
3053
2985
  }
3054
2986
 
3055
- const useStyles$a = core$1.createStyles((theme) => ({
2987
+ const useStyles$c = core$1.createStyles(() => ({
2988
+ root: {
2989
+ '@media (max-width: 800px)': {
2990
+ paddingLeft: 4,
2991
+ paddingRight: 4,
2992
+ },
2993
+ },
2994
+ }));
2995
+ function Container(props) {
2996
+ const { children, ...others } = props;
2997
+ const { classes } = useStyles$c();
2998
+ return (React.createElement(core$1.Container, { className: classes.root, ...others }, children));
2999
+ }
3000
+
3001
+ const useStyles$b = core$1.createStyles((theme) => ({
3056
3002
  noteBody: { fontSize: theme.fontSizes.sm },
3057
3003
  noteCite: { fontSize: theme.fontSizes.xs, marginBlockStart: 3 },
3058
3004
  noteRoot: { padding: 5 },
3059
3005
  }));
3060
3006
  function NoteDisplay({ value }) {
3061
- const { classes } = useStyles$a();
3007
+ const { classes } = useStyles$b();
3062
3008
  if (!value) {
3063
3009
  return null;
3064
3010
  }
@@ -3151,7 +3097,7 @@
3151
3097
  return React.createElement(core$1.Badge, { color: statusToColor[props.status] }, props.status);
3152
3098
  }
3153
3099
 
3154
- const useStyles$9 = core$1.createStyles((theme) => ({
3100
+ const useStyles$a = core$1.createStyles((theme) => ({
3155
3101
  table: {
3156
3102
  border: `0.1px solid ${theme.colors.gray[5]}`,
3157
3103
  borderCollapse: 'collapse',
@@ -3246,7 +3192,7 @@
3246
3192
  core.formatDateTime(specimen.receivedTime)))))))));
3247
3193
  }
3248
3194
  function ObservationTable(props) {
3249
- const { classes } = useStyles$9();
3195
+ const { classes } = useStyles$a();
3250
3196
  return (React.createElement("table", { className: classes.table },
3251
3197
  React.createElement("thead", null,
3252
3198
  React.createElement("tr", null,
@@ -3260,7 +3206,7 @@
3260
3206
  React.createElement("tbody", null, props.value?.map((observation, index) => (React.createElement(ObservationRow, { key: `obs-${observation.id}-${index}`, hideObservationNotes: props.hideObservationNotes, value: observation }))))));
3261
3207
  }
3262
3208
  function ObservationRow(props) {
3263
- const { classes, cx } = useStyles$9();
3209
+ const { classes, cx } = useStyles$a();
3264
3210
  const observation = useResource(props.value);
3265
3211
  if (!observation) {
3266
3212
  return null;
@@ -3310,6 +3256,92 @@
3310
3256
  return code === 'AA' || code === 'LL' || code === 'HH' || code === 'A';
3311
3257
  }
3312
3258
 
3259
+ /**
3260
+ * Parses an HTML form and returns the result as a JavaScript object.
3261
+ * @param form The HTML form element.
3262
+ */
3263
+ function parseForm(form) {
3264
+ const result = {};
3265
+ for (const element of Array.from(form.elements)) {
3266
+ if (element instanceof HTMLInputElement) {
3267
+ parseInputElement(result, element);
3268
+ }
3269
+ else if (element instanceof HTMLTextAreaElement) {
3270
+ result[element.name] = element.value;
3271
+ }
3272
+ else if (element instanceof HTMLSelectElement) {
3273
+ parseSelectElement(result, element);
3274
+ }
3275
+ }
3276
+ return result;
3277
+ }
3278
+ /**
3279
+ * Parses an HTML input element.
3280
+ * Sets the name/value pair in the result,
3281
+ * but only if the element is enabled and checked.
3282
+ * @param el The input element.
3283
+ * @param result The result builder.
3284
+ */
3285
+ function parseInputElement(result, el) {
3286
+ if (el.disabled) {
3287
+ // Ignore disabled elements
3288
+ return;
3289
+ }
3290
+ if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
3291
+ // Ignore unchecked radio or checkbox elements
3292
+ return;
3293
+ }
3294
+ result[el.name] = el.value;
3295
+ }
3296
+ /**
3297
+ * Parses an HTML select element.
3298
+ * Sets the name/value pair if one is selected.
3299
+ * @param result The result builder.
3300
+ * @param el The select element.
3301
+ */
3302
+ function parseSelectElement(result, el) {
3303
+ result[el.name] = el.value;
3304
+ }
3305
+
3306
+ function Form(props) {
3307
+ return (React.createElement("form", { style: props.style, "data-testid": props.testid, onSubmit: (e) => {
3308
+ e.preventDefault();
3309
+ const formData = parseForm(e.target);
3310
+ if (props.onSubmit) {
3311
+ props.onSubmit(formData);
3312
+ }
3313
+ } }, props.children));
3314
+ }
3315
+
3316
+ const useStyles$9 = core$1.createStyles((theme, { width, fill }) => ({
3317
+ paper: {
3318
+ maxWidth: width,
3319
+ margin: `${theme.spacing.xl} auto`,
3320
+ padding: fill ? 0 : theme.spacing.md,
3321
+ '@media (max-width: 800px)': {
3322
+ padding: fill ? 0 : 8,
3323
+ },
3324
+ '& img': {
3325
+ width: '100%',
3326
+ maxWidth: '100%',
3327
+ },
3328
+ '& video': {
3329
+ width: '100%',
3330
+ maxWidth: '100%',
3331
+ },
3332
+ },
3333
+ }));
3334
+ const defaultProps$1 = {
3335
+ shadow: 'xs',
3336
+ radius: 'md',
3337
+ withBorder: true,
3338
+ };
3339
+ function Panel(props) {
3340
+ const { className, children, width, fill, unstyled, ...others } = core$1.useComponentDefaultProps('Panel', defaultProps$1, props);
3341
+ const { classes, cx } = useStyles$9({ width, fill }, { name: 'Panel', unstyled });
3342
+ return (React.createElement(core$1.Paper, { className: cx(classes.paper, className), ...others }, children));
3343
+ }
3344
+
3313
3345
  const useStyles$8 = core$1.createStyles((theme) => ({
3314
3346
  root: {
3315
3347
  borderCollapse: 'collapse',
@@ -3400,29 +3432,6 @@
3400
3432
  }, ignoreMissingValues: props.ignoreMissingValues }));
3401
3433
  }
3402
3434
 
3403
- /**
3404
- * ErrorBoundary is a React component that handles errors in its child components.
3405
- * See: https://reactjs.org/docs/error-boundaries.html
3406
- */
3407
- class ErrorBoundary extends React.Component {
3408
- constructor(props) {
3409
- super(props);
3410
- this.state = {};
3411
- }
3412
- static getDerivedStateFromError(error) {
3413
- return { error };
3414
- }
3415
- componentDidCatch(error, errorInfo) {
3416
- console.error('Uncaught error:', error, errorInfo);
3417
- }
3418
- render() {
3419
- if (this.state.error) {
3420
- return (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), title: "Something went wrong", color: "red" }, core.normalizeErrorString(this.state.error)));
3421
- }
3422
- return this.props.children;
3423
- }
3424
- }
3425
-
3426
3435
  function Timeline(props) {
3427
3436
  return React.createElement(Container, null, props.children);
3428
3437
  }
@@ -3787,6 +3796,12 @@
3787
3796
  } }));
3788
3797
  }
3789
3798
 
3799
+ function Document(props) {
3800
+ const { children, ...others } = props;
3801
+ return (React.createElement(Container, null,
3802
+ React.createElement(Panel, { ...others }, children)));
3803
+ }
3804
+
3790
3805
  function EncounterTimeline(props) {
3791
3806
  return (React.createElement(ResourceTimeline, { value: props.encounter, loadTimelineResources: async (medplum, _resourceType, id) => {
3792
3807
  return Promise.allSettled([
@@ -5056,8 +5071,8 @@
5056
5071
  return (React.createElement("div", { className: classes.root, "data-testid": "search-control" },
5057
5072
  !props.hideToolbar && (React.createElement(core$1.Group, { position: "apart", mb: "xl" },
5058
5073
  React.createElement(core$1.Group, { spacing: 2 },
5059
- React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconFilter, { size: iconSize }), onClick: () => setState({ ...stateRef.current, fieldEditorVisible: true }) }, "Fields"),
5060
- React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconColumns, { size: iconSize }), onClick: () => setState({ ...stateRef.current, filterEditorVisible: true }) }, "Filters"),
5074
+ React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconColumns, { size: iconSize }), onClick: () => setState({ ...stateRef.current, fieldEditorVisible: true }) }, "Fields"),
5075
+ React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconFilter, { size: iconSize }), onClick: () => setState({ ...stateRef.current, filterEditorVisible: true }) }, "Filters"),
5061
5076
  props.onNew && (React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconFilePlus, { size: iconSize }), onClick: props.onNew }, "New...")),
5062
5077
  !isMobile && isExportPassed() && (React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconTableExport, { size: iconSize }), onClick: props.onExport ? props.onExport : () => setState({ ...stateRef.current, exportDialogVisible: true }) }, "Export...")),
5063
5078
  !isMobile && props.onDelete && (React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconTrash, { size: iconSize }), onClick: () => props.onDelete(Object.keys(state.selected)) }, "Delete...")),
@@ -5293,6 +5308,24 @@
5293
5308
  }
5294
5309
  const MemoizedFhirPathTable = React.memo(FhirPathTable);
5295
5310
 
5311
+ function Logo(props) {
5312
+ return (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 491 491", style: { width: props.size, height: props.size } },
5313
+ React.createElement("title", null, "Medplum Logo"),
5314
+ React.createElement("path", { fill: props.fill || '#ad7136', d: "M282 67c6-16 16-29 29-40L289 0c-22 17-37 41-43 68l17 23 19-24z" }),
5315
+ React.createElement("path", { fill: props.fill || '#946af9', d: "M311 63c-17 0-33 4-48 11-16-7-32-11-49-11-87 0-158 96-158 214s71 214 158 214c17 0 33-4 49-11 15 7 31 11 48 11 87 0 158-96 158-214S398 63 311 63z" }),
5316
+ React.createElement("path", { fill: props.fill || '#7857c5', d: "M231 489l-17 2c-87 0-158-96-158-214S127 63 214 63l17 1c-39 12-70 102-70 213s31 201 70 212z" }),
5317
+ React.createElement("path", { fill: props.fill || '#40bc26', d: "M207 220a176 176 0 01-177 43A176 176 0 01251 43l1 5c17 59 2 125-45 172z" }),
5318
+ React.createElement("path", { fill: props.fill || '#33961e', d: "M252 48A421 421 0 0057 270l-27-7A176 176 0 01251 43l1 5z" })));
5319
+ }
5320
+
5321
+ function OperationOutcomeAlert(props) {
5322
+ const issues = props.outcome?.issue || props.issues;
5323
+ if (!issues) {
5324
+ return null;
5325
+ }
5326
+ return (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), color: "red" }, issues.map((issue) => (React.createElement("div", { "data-testid": "text-field-error", key: issue.details?.text }, issue.details?.text)))));
5327
+ }
5328
+
5296
5329
  function PatientTimeline(props) {
5297
5330
  const loadTimelineResources = React.useCallback((medplum, _resourceType, id) => {
5298
5331
  return Promise.allSettled([
@@ -6918,9 +6951,455 @@
6918
6951
  }) }));
6919
6952
  }
6920
6953
 
6954
+ function NewProjectForm(props) {
6955
+ const medplum = useMedplum();
6956
+ const [outcome, setOutcome] = React.useState();
6957
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: async (formData) => {
6958
+ try {
6959
+ props.handleAuthResponse(await medplum.startNewProject({
6960
+ login: props.login,
6961
+ projectName: formData.projectName,
6962
+ }));
6963
+ }
6964
+ catch (err) {
6965
+ setOutcome(err);
6966
+ }
6967
+ } },
6968
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
6969
+ React.createElement(Logo, { size: 32 }),
6970
+ React.createElement(core$1.Title, null, "Create project")),
6971
+ React.createElement(core$1.Stack, { spacing: "xl" },
6972
+ React.createElement(core$1.TextInput, { name: "projectName", label: "Project Name", placeholder: "My Project", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'firstName') }),
6973
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
6974
+ "By clicking submit you agree to the Medplum",
6975
+ ' ',
6976
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/privacy" }, "Privacy\u00A0Policy"),
6977
+ ' and ',
6978
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/terms" }, "Terms\u00A0of\u00A0Service"),
6979
+ ".")),
6980
+ React.createElement(core$1.Group, { position: "right", mt: "xl", noWrap: true },
6981
+ React.createElement(core$1.Button, { type: "submit" }, "Create project"))));
6982
+ }
6983
+
6984
+ /**
6985
+ * Dynamically creates a script tag for the specified JavaScript file.
6986
+ * @param src The JavaScript file URL.
6987
+ */
6988
+ function createScriptTag(src, onload) {
6989
+ const head = document.getElementsByTagName('head')[0];
6990
+ const script = document.createElement('script');
6991
+ script.async = true;
6992
+ script.src = src;
6993
+ script.onload = onload || null;
6994
+ head.appendChild(script);
6995
+ }
6996
+
6997
+ function GoogleButton(props) {
6998
+ const medplum = useMedplum();
6999
+ const { googleClientId, handleGoogleCredential } = props;
7000
+ const parentRef = React.useRef(null);
7001
+ const [scriptLoaded, setScriptLoaded] = React.useState(typeof google !== 'undefined');
7002
+ const [initialized, setInitialized] = React.useState(false);
7003
+ const [buttonRendered, setButtonRendered] = React.useState(false);
7004
+ React.useEffect(() => {
7005
+ if (typeof google === 'undefined') {
7006
+ createScriptTag('https://accounts.google.com/gsi/client', () => setScriptLoaded(true));
7007
+ return;
7008
+ }
7009
+ if (!initialized) {
7010
+ google.accounts.id.initialize({
7011
+ client_id: googleClientId,
7012
+ callback: handleGoogleCredential,
7013
+ });
7014
+ setInitialized(true);
7015
+ }
7016
+ if (parentRef.current && !buttonRendered) {
7017
+ google.accounts.id.renderButton(parentRef.current, {});
7018
+ setButtonRendered(true);
7019
+ }
7020
+ }, [medplum, googleClientId, initialized, scriptLoaded, parentRef, buttonRendered, handleGoogleCredential]);
7021
+ if (!googleClientId) {
7022
+ return null;
7023
+ }
7024
+ return React.createElement("div", { ref: parentRef });
7025
+ }
7026
+ function getGoogleClientId(clientId) {
7027
+ if (clientId) {
7028
+ return clientId;
7029
+ }
7030
+ if (typeof window !== 'undefined') {
7031
+ const origin = window.location.protocol + '//' + window.location.host;
7032
+ const authorizedOrigins = "undefined"?.split(',') ?? [];
7033
+ if (authorizedOrigins.includes(origin)) {
7034
+ return "undefined";
7035
+ }
7036
+ }
7037
+ return undefined;
7038
+ }
7039
+
7040
+ /**
7041
+ * Dynamically loads the recaptcha script.
7042
+ * We do not want to load the script on page load unless the user needs it.
7043
+ * @param siteKey The reCAPTCHA site key, available from the reCAPTCHA admin page.
7044
+ */
7045
+ function initRecaptcha(siteKey) {
7046
+ if (typeof grecaptcha === 'undefined') {
7047
+ createScriptTag('https://www.google.com/recaptcha/api.js?render=' + siteKey);
7048
+ }
7049
+ }
7050
+ /**
7051
+ * Starts a request to generate a recapcha token.
7052
+ * @param siteKey The reCAPTCHA site key, available from the reCAPTCHA admin page.
7053
+ * @returns Promise to a recaptcha token for the current user.
7054
+ */
7055
+ function getRecaptcha(siteKey) {
7056
+ return new Promise((resolve, reject) => {
7057
+ grecaptcha.ready(async () => {
7058
+ try {
7059
+ resolve(await grecaptcha.execute(siteKey, { action: 'submit' }));
7060
+ }
7061
+ catch (err) {
7062
+ reject(err);
7063
+ }
7064
+ });
7065
+ });
7066
+ }
7067
+
7068
+ function NewUserForm(props) {
7069
+ const googleClientId = getGoogleClientId(props.googleClientId);
7070
+ const recaptchaSiteKey = props.recaptchaSiteKey;
7071
+ const medplum = useMedplum();
7072
+ const [outcome, setOutcome] = React.useState();
7073
+ const issues = getIssuesForExpression(outcome, undefined);
7074
+ React.useEffect(() => {
7075
+ if (recaptchaSiteKey) {
7076
+ initRecaptcha(recaptchaSiteKey);
7077
+ }
7078
+ }, [recaptchaSiteKey]);
7079
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: async (formData) => {
7080
+ try {
7081
+ let recaptchaToken = '';
7082
+ if (recaptchaSiteKey) {
7083
+ recaptchaToken = await getRecaptcha(recaptchaSiteKey);
7084
+ }
7085
+ props.handleAuthResponse(await medplum.startNewUser({
7086
+ projectId: props.projectId,
7087
+ firstName: formData.firstName,
7088
+ lastName: formData.lastName,
7089
+ email: formData.email,
7090
+ password: formData.password,
7091
+ remember: formData.remember === 'true',
7092
+ recaptchaSiteKey,
7093
+ recaptchaToken,
7094
+ }));
7095
+ }
7096
+ catch (err) {
7097
+ setOutcome(err);
7098
+ }
7099
+ } },
7100
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, props.children),
7101
+ React.createElement(OperationOutcomeAlert, { issues: issues }),
7102
+ googleClientId && (React.createElement(React.Fragment, null,
7103
+ React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
7104
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: async (response) => {
7105
+ try {
7106
+ props.handleAuthResponse(await medplum.startGoogleLogin({
7107
+ googleClientId: response.clientId,
7108
+ googleCredential: response.credential,
7109
+ createUser: true,
7110
+ }));
7111
+ }
7112
+ catch (err) {
7113
+ setOutcome(err);
7114
+ }
7115
+ } })),
7116
+ React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
7117
+ React.createElement(core$1.Stack, { spacing: "xl" },
7118
+ React.createElement(core$1.TextInput, { name: "firstName", type: "text", label: "First name", placeholder: "First name", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'firstName') }),
7119
+ React.createElement(core$1.TextInput, { name: "lastName", type: "text", label: "Last name", placeholder: "Last name", required: true, error: getErrorsForInput(outcome, 'lastName') }),
7120
+ React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, error: getErrorsForInput(outcome, 'email') }),
7121
+ React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, error: getErrorsForInput(outcome, 'password') }),
7122
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
7123
+ "By clicking submit you agree to the Medplum",
7124
+ ' ',
7125
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/privacy" }, "Privacy\u00A0Policy"),
7126
+ ' and ',
7127
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/terms" }, "Terms\u00A0of\u00A0Service"),
7128
+ "."),
7129
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
7130
+ "This site is protected by reCAPTCHA and the Google",
7131
+ ' ',
7132
+ React.createElement(core$1.Anchor, { href: "https://policies.google.com/privacy" }, "Privacy\u00A0Policy"),
7133
+ ' and ',
7134
+ React.createElement(core$1.Anchor, { href: "https://policies.google.com/terms" }, "Terms\u00A0of\u00A0Service"),
7135
+ " apply.")),
7136
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", noWrap: true },
7137
+ React.createElement(core$1.Checkbox, { name: "remember", label: "Remember me", size: "xs" }),
7138
+ React.createElement(core$1.Button, { type: "submit" }, "Create account"))));
7139
+ }
7140
+
7141
+ function RegisterForm(props) {
7142
+ const { type, projectId, googleClientId, recaptchaSiteKey, onSuccess } = props;
7143
+ const medplum = useMedplum();
7144
+ const [login, setLogin] = React.useState(undefined);
7145
+ const [outcome, setOutcome] = React.useState();
7146
+ React.useEffect(() => {
7147
+ if (type === 'patient' && login) {
7148
+ medplum
7149
+ .startNewPatient({ login, projectId: projectId })
7150
+ .then((response) => medplum.processCode(response.code))
7151
+ .then(() => onSuccess())
7152
+ .catch((err) => setOutcome(err));
7153
+ }
7154
+ }, [medplum, type, projectId, login, onSuccess]);
7155
+ function handleAuthResponse(response) {
7156
+ if (response.code) {
7157
+ medplum
7158
+ .processCode(response.code)
7159
+ .then(() => onSuccess())
7160
+ .catch(console.log);
7161
+ }
7162
+ else if (response.login) {
7163
+ setLogin(response.login);
7164
+ }
7165
+ }
7166
+ return (React.createElement(Document, { width: 450 },
7167
+ outcome && React.createElement("pre", null, JSON.stringify(outcome, null, 2)),
7168
+ !login && (React.createElement(NewUserForm, { projectId: projectId, googleClientId: googleClientId, recaptchaSiteKey: recaptchaSiteKey, handleAuthResponse: handleAuthResponse }, props.children)),
7169
+ login && type === 'project' && React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse })));
7170
+ }
7171
+
7172
+ function AuthenticationForm(props) {
7173
+ const [email, setEmail] = React.useState();
7174
+ if (!email) {
7175
+ return React.createElement(EmailForm, { setEmail: setEmail, ...props });
7176
+ }
7177
+ else {
7178
+ return React.createElement(PasswordForm, { email: email, ...props });
7179
+ }
7180
+ }
7181
+ function EmailForm(props) {
7182
+ const { setEmail, onRegister, handleAuthResponse, children, ...baseLoginRequest } = props;
7183
+ const medplum = useMedplum();
7184
+ const googleClientId = !props.disableGoogleAuth && getGoogleClientId(props.googleClientId);
7185
+ const isExternalAuth = React.useCallback(async (authMethod) => {
7186
+ if (!authMethod.authorizeUrl) {
7187
+ return false;
7188
+ }
7189
+ const state = JSON.stringify({
7190
+ ...(await medplum.ensureCodeChallenge(baseLoginRequest)),
7191
+ domain: authMethod.domain,
7192
+ });
7193
+ const url = new URL(authMethod.authorizeUrl);
7194
+ url.searchParams.set('state', state);
7195
+ window.location.assign(url.toString());
7196
+ return true;
7197
+ }, [medplum, baseLoginRequest]);
7198
+ const handleSubmit = React.useCallback(async (formData) => {
7199
+ const authMethod = await medplum.post('auth/method', { email: formData.email });
7200
+ if (!(await isExternalAuth(authMethod))) {
7201
+ setEmail(formData.email);
7202
+ }
7203
+ }, [medplum, isExternalAuth, setEmail]);
7204
+ const handleGoogleCredential = React.useCallback(async (response) => {
7205
+ const authResponse = await medplum.startGoogleLogin({
7206
+ ...baseLoginRequest,
7207
+ googleCredential: response.credential,
7208
+ });
7209
+ if (!(await isExternalAuth(authResponse))) {
7210
+ handleAuthResponse(authResponse);
7211
+ }
7212
+ }, [medplum, baseLoginRequest, isExternalAuth, handleAuthResponse]);
7213
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
7214
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
7215
+ googleClientId && (React.createElement(React.Fragment, null,
7216
+ React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
7217
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: handleGoogleCredential })),
7218
+ React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
7219
+ React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, autoFocus: true }),
7220
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
7221
+ React.createElement("div", null, onRegister && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onRegister, size: "xs" }, "Register"))),
7222
+ React.createElement(core$1.Button, { type: "submit" }, "Next"))));
7223
+ }
7224
+ function PasswordForm(props) {
7225
+ const { onForgotPassword, handleAuthResponse, children, ...baseLoginRequest } = props;
7226
+ const medplum = useMedplum();
7227
+ const [outcome, setOutcome] = React.useState();
7228
+ const issues = getIssuesForExpression(outcome, undefined);
7229
+ const handleSubmit = React.useCallback((formData) => {
7230
+ medplum
7231
+ .startLogin({
7232
+ ...baseLoginRequest,
7233
+ password: formData.password,
7234
+ remember: formData.remember === 'on',
7235
+ })
7236
+ .then(handleAuthResponse)
7237
+ .catch((err) => setOutcome(core.normalizeOperationOutcome(err)));
7238
+ }, [medplum, baseLoginRequest, handleAuthResponse]);
7239
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
7240
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
7241
+ React.createElement(OperationOutcomeAlert, { issues: issues }),
7242
+ React.createElement(core$1.Stack, { spacing: "xl" },
7243
+ React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, error: getErrorsForInput(outcome, 'password') })),
7244
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
7245
+ onForgotPassword && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onForgotPassword, size: "xs" }, "Forgot password")),
7246
+ React.createElement(core$1.Checkbox, { id: "remember", name: "remember", label: "Remember me", size: "xs", sx: { lineHeight: 1 } }),
7247
+ React.createElement(core$1.Button, { type: "submit" }, "Sign in"))));
7248
+ }
7249
+
7250
+ function ChooseProfileForm(props) {
7251
+ const medplum = useMedplum();
7252
+ const [outcome, setOutcome] = React.useState();
7253
+ return (React.createElement(core$1.Stack, null,
7254
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7255
+ React.createElement(Logo, { size: 32 }),
7256
+ React.createElement(core$1.Title, { order: 3 }, "Choose profile")),
7257
+ React.createElement(OperationOutcomeAlert, { outcome: outcome }),
7258
+ props.memberships.map((membership) => (React.createElement(core$1.UnstyledButton, { key: membership.id, onClick: () => {
7259
+ medplum
7260
+ .post('auth/profile', {
7261
+ login: props.login,
7262
+ profile: membership.id,
7263
+ })
7264
+ .then(props.handleAuthResponse)
7265
+ .catch((err) => setOutcome(core.normalizeOperationOutcome(err)));
7266
+ } },
7267
+ React.createElement(core$1.Group, null,
7268
+ React.createElement(core$1.Avatar, { radius: "xl" }),
7269
+ React.createElement("div", { style: { flex: 1 } },
7270
+ React.createElement(core$1.Text, { size: "sm", weight: 500 }, membership.profile?.display),
7271
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, membership.project?.display))))))));
7272
+ }
7273
+
7274
+ function ChooseScopeForm(props) {
7275
+ const medplum = useMedplum();
7276
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
7277
+ medplum
7278
+ .post('auth/scope', {
7279
+ login: props.login,
7280
+ scope: Object.keys(formData).join(' '),
7281
+ })
7282
+ .then(props.handleAuthResponse)
7283
+ .catch(console.log);
7284
+ } },
7285
+ React.createElement(core$1.Stack, null,
7286
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7287
+ React.createElement(Logo, { size: 32 }),
7288
+ React.createElement(core$1.Title, null, "Choose scope")),
7289
+ React.createElement(core$1.Stack, null, (props.scope || 'openid').split(' ').map((scopeName) => (React.createElement(core$1.Checkbox, { key: scopeName, id: scopeName, name: scopeName, label: scopeName, defaultChecked: true })))),
7290
+ React.createElement(core$1.Group, { position: "right", mt: "xl" },
7291
+ React.createElement(core$1.Button, { type: "submit" }, "Set scope")))));
7292
+ }
7293
+
7294
+ function MfaForm(props) {
7295
+ const medplum = useMedplum();
7296
+ const [errorMessage, setErrorMessage] = React.useState(undefined);
7297
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
7298
+ setErrorMessage(undefined);
7299
+ medplum
7300
+ .post('auth/mfa/verify', {
7301
+ login: props.login,
7302
+ token: formData.token,
7303
+ })
7304
+ .then(props.handleAuthResponse)
7305
+ .catch((err) => setErrorMessage(core.normalizeErrorString(err)));
7306
+ } },
7307
+ React.createElement(core$1.Stack, null,
7308
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7309
+ React.createElement(Logo, { size: 32 }),
7310
+ React.createElement(core$1.Title, null, "Enter MFA code")),
7311
+ errorMessage && (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), title: "Error", color: "red" }, errorMessage)),
7312
+ React.createElement(core$1.Stack, null,
7313
+ React.createElement(core$1.TextInput, { name: "token", label: "MFA code", required: true })),
7314
+ React.createElement(core$1.Group, { position: "right", mt: "xl" },
7315
+ React.createElement(core$1.Button, { type: "submit" }, "Submit code")))));
7316
+ }
7317
+
7318
+ /**
7319
+ * The SignInForm component allows users to sign in to Medplum.
7320
+ *
7321
+ * "Signing in" is a multi-step process:
7322
+ * 1) Authentication - identify the user
7323
+ * 2) MFA - If MFA is enabled, prompt for MFA code
7324
+ * 3) Choose profile - If the user has multiple profiles, prompt to choose one
7325
+ * 4) Choose scope - If the user has multiple scopes, prompt to choose one
7326
+ * 5) Success - Return to the caller with either a code or a redirect
7327
+ */
7328
+ function SignInForm(props) {
7329
+ const { chooseScopes, onSuccess, onForgotPassword, onRegister, onCode, ...baseLoginRequest } = props;
7330
+ const medplum = useMedplum();
7331
+ const [login, setLogin] = React.useState(undefined);
7332
+ const [mfaRequired, setAuthenticatorRequired] = React.useState(false);
7333
+ const [memberships, setMemberships] = React.useState(undefined);
7334
+ const handleCode = React.useCallback((code) => {
7335
+ if (onCode) {
7336
+ onCode(code);
7337
+ }
7338
+ else {
7339
+ medplum
7340
+ .processCode(code)
7341
+ .then(() => {
7342
+ if (onSuccess) {
7343
+ onSuccess();
7344
+ }
7345
+ })
7346
+ .catch(console.log);
7347
+ }
7348
+ }, [medplum, onCode, onSuccess]);
7349
+ const handleAuthResponse = React.useCallback((response) => {
7350
+ setAuthenticatorRequired(!!response.mfaRequired);
7351
+ if (response.login) {
7352
+ setLogin(response.login);
7353
+ }
7354
+ if (response.memberships) {
7355
+ setMemberships(response.memberships);
7356
+ }
7357
+ if (response.code) {
7358
+ if (chooseScopes) {
7359
+ setMemberships(undefined);
7360
+ }
7361
+ else {
7362
+ handleCode(response.code);
7363
+ }
7364
+ }
7365
+ }, [chooseScopes, handleCode]);
7366
+ const handleScopeResponse = React.useCallback((response) => {
7367
+ handleCode(response.code);
7368
+ }, [handleCode]);
7369
+ React.useEffect(() => {
7370
+ if (props.login) {
7371
+ medplum
7372
+ .get('auth/login/' + props.login)
7373
+ .then(handleAuthResponse)
7374
+ .catch(console.error);
7375
+ }
7376
+ }, [medplum, props, handleAuthResponse]);
7377
+ return (React.createElement(Document, { width: 450 }, (() => {
7378
+ if (!login) {
7379
+ return (React.createElement(AuthenticationForm, { onForgotPassword: onForgotPassword, onRegister: onRegister, handleAuthResponse: handleAuthResponse, disableGoogleAuth: props.disableGoogleAuth, ...baseLoginRequest }, props.children));
7380
+ }
7381
+ else if (mfaRequired) {
7382
+ return React.createElement(MfaForm, { login: login, handleAuthResponse: handleAuthResponse });
7383
+ }
7384
+ else if (memberships) {
7385
+ return React.createElement(ChooseProfileForm, { login: login, memberships: memberships, handleAuthResponse: handleAuthResponse });
7386
+ }
7387
+ else if (props.projectId === 'new') {
7388
+ return React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse });
7389
+ }
7390
+ else if (props.chooseScopes) {
7391
+ return React.createElement(ChooseScopeForm, { login: login, scope: props.scope, handleAuthResponse: handleScopeResponse });
7392
+ }
7393
+ else {
7394
+ return React.createElement("div", null, "Success");
7395
+ }
7396
+ })()));
7397
+ }
7398
+
6921
7399
  exports.AddressDisplay = AddressDisplay;
6922
7400
  exports.AddressInput = AddressInput;
6923
7401
  exports.AnnotationInput = AnnotationInput;
7402
+ exports.AppShell = AppShell;
6924
7403
  exports.AsyncAutocomplete = AsyncAutocomplete;
6925
7404
  exports.AttachmentArrayDisplay = AttachmentArrayDisplay;
6926
7405
  exports.AttachmentArrayInput = AttachmentArrayInput;
@@ -6954,10 +7433,12 @@
6954
7433
  exports.FhirPathTable = FhirPathTable;
6955
7434
  exports.Form = Form;
6956
7435
  exports.FormSection = FormSection;
7436
+ exports.Header = Header;
6957
7437
  exports.HumanNameDisplay = HumanNameDisplay;
6958
7438
  exports.HumanNameInput = HumanNameInput;
6959
7439
  exports.IdentifierDisplay = IdentifierDisplay;
6960
7440
  exports.IdentifierInput = IdentifierInput;
7441
+ exports.Loading = Loading;
6961
7442
  exports.Logo = Logo;
6962
7443
  exports.MedplumLink = MedplumLink;
6963
7444
  exports.MedplumProvider = MedplumProvider;
@@ -6965,6 +7446,7 @@
6965
7446
  exports.MemoizedSearchControl = MemoizedSearchControl;
6966
7447
  exports.MoneyDisplay = MoneyDisplay;
6967
7448
  exports.MoneyInput = MoneyInput;
7449
+ exports.Navbar = Navbar;
6968
7450
  exports.ObservationTable = ObservationTable;
6969
7451
  exports.OperationOutcomeAlert = OperationOutcomeAlert;
6970
7452
  exports.Panel = Panel;