@medplum/react 2.0.16 → 2.0.18

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 (221) hide show
  1. package/dist/cjs/index.cjs +1642 -1095
  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 +120 -0
  11. package/dist/esm/AppShell/Navbar.mjs.map +1 -0
  12. package/dist/esm/BookmarkDialog/BookmarkDialog.mjs +49 -0
  13. package/dist/esm/BookmarkDialog/BookmarkDialog.mjs.map +1 -0
  14. package/dist/esm/DefaultResourceTimeline/DefaultResourceTimeline.mjs +6 -1
  15. package/dist/esm/DefaultResourceTimeline/DefaultResourceTimeline.mjs.map +1 -1
  16. package/dist/esm/GoogleButton/GoogleButton.mjs +2 -2
  17. package/dist/esm/GoogleButton/GoogleButton.mjs.map +1 -1
  18. package/dist/esm/Loading/Loading.mjs +10 -0
  19. package/dist/esm/Loading/Loading.mjs.map +1 -0
  20. package/dist/esm/MedplumLink/MedplumLink.mjs +1 -1
  21. package/dist/esm/MedplumLink/MedplumLink.mjs.map +1 -1
  22. package/dist/esm/PatientTimeline/PatientTimeline.mjs +10 -7
  23. package/dist/esm/PatientTimeline/PatientTimeline.mjs.map +1 -1
  24. package/dist/esm/SearchControl/SearchControl.mjs +3 -3
  25. package/dist/esm/SearchControl/SearchControl.mjs.map +1 -1
  26. package/dist/esm/SearchControl/SearchUtils.mjs +1 -0
  27. package/dist/esm/SearchControl/SearchUtils.mjs.map +1 -1
  28. package/dist/esm/SearchFilterEditor/SearchFilterEditor.mjs +1 -1
  29. package/dist/esm/SearchFilterEditor/SearchFilterEditor.mjs.map +1 -1
  30. package/dist/esm/ServiceRequestTimeline/ServiceRequestTimeline.mjs +7 -4
  31. package/dist/esm/ServiceRequestTimeline/ServiceRequestTimeline.mjs.map +1 -1
  32. package/dist/esm/auth/AuthenticationForm.mjs +1 -1
  33. package/dist/esm/auth/AuthenticationForm.mjs.map +1 -1
  34. package/dist/esm/auth/NewUserForm.mjs.map +1 -1
  35. package/dist/esm/auth/RegisterForm.mjs.map +1 -1
  36. package/dist/esm/index.min.mjs +1 -1
  37. package/dist/esm/index.mjs +7 -3
  38. package/dist/esm/index.mjs.map +1 -1
  39. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/createReactComponent.mjs +1 -1
  40. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/createReactComponent.mjs.map +1 -1
  41. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/defaultAttributes.mjs +1 -1
  42. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/defaultAttributes.mjs.map +1 -1
  43. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAdjustmentsHorizontal.mjs +1 -1
  44. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAdjustmentsHorizontal.mjs.map +1 -1
  45. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAlertCircle.mjs +1 -1
  46. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconAlertCircle.mjs.map +1 -1
  47. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleach.mjs +1 -1
  48. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleach.mjs.map +1 -1
  49. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleachOff.mjs +1 -1
  50. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBleachOff.mjs.map +1 -1
  51. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBoxMultiple.mjs +1 -1
  52. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBoxMultiple.mjs.map +1 -1
  53. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBracketsContain.mjs +1 -1
  54. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBracketsContain.mjs.map +1 -1
  55. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucket.mjs +1 -1
  56. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucket.mjs.map +1 -1
  57. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucketOff.mjs +1 -1
  58. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconBucketOff.mjs.map +1 -1
  59. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCalendar.mjs +1 -1
  60. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCalendar.mjs.map +1 -1
  61. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheck.mjs +1 -1
  62. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheck.mjs.map +1 -1
  63. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheckbox.mjs +1 -1
  64. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCheckbox.mjs.map +1 -1
  65. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconChevronDown.mjs +12 -0
  66. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconChevronDown.mjs.map +1 -0
  67. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCircleMinus.mjs +1 -1
  68. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCircleMinus.mjs.map +1 -1
  69. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCirclePlus.mjs +1 -1
  70. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCirclePlus.mjs.map +1 -1
  71. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCloudUpload.mjs +1 -1
  72. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCloudUpload.mjs.map +1 -1
  73. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconColumns.mjs +1 -1
  74. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconColumns.mjs.map +1 -1
  75. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCurrencyDollar.mjs +1 -1
  76. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconCurrencyDollar.mjs.map +1 -1
  77. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconDots.mjs +1 -1
  78. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconDots.mjs.map +1 -1
  79. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEdit.mjs +1 -1
  80. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEdit.mjs.map +1 -1
  81. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqual.mjs +1 -1
  82. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqual.mjs.map +1 -1
  83. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqualNot.mjs +1 -1
  84. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconEqualNot.mjs.map +1 -1
  85. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFileAlert.mjs +1 -1
  86. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFileAlert.mjs.map +1 -1
  87. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilePlus.mjs +1 -1
  88. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilePlus.mjs.map +1 -1
  89. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilter.mjs +1 -1
  90. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconFilter.mjs.map +1 -1
  91. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconListDetails.mjs +1 -1
  92. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconListDetails.mjs.map +1 -1
  93. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconLogout.mjs +19 -0
  94. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconLogout.mjs.map +1 -0
  95. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathGreater.mjs +1 -1
  96. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathGreater.mjs.map +1 -1
  97. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathLower.mjs +1 -1
  98. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMathLower.mjs.map +1 -1
  99. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMessage.mjs +1 -1
  100. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconMessage.mjs.map +1 -1
  101. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPin.mjs +1 -1
  102. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPin.mjs.map +1 -1
  103. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPinnedOff.mjs +1 -1
  104. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPinnedOff.mjs.map +1 -1
  105. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPlus.mjs +13 -0
  106. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPlus.mjs.map +1 -0
  107. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSearch.mjs +13 -0
  108. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSearch.mjs.map +1 -0
  109. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSettings.mjs +1 -1
  110. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSettings.mjs.map +1 -1
  111. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortAscending.mjs +1 -1
  112. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortAscending.mjs.map +1 -1
  113. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortDescending.mjs +1 -1
  114. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSortDescending.mjs.map +1 -1
  115. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSquare.mjs +1 -1
  116. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSquare.mjs.map +1 -1
  117. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSwitchHorizontal.mjs +19 -0
  118. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconSwitchHorizontal.mjs.map +1 -0
  119. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTableExport.mjs +1 -1
  120. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTableExport.mjs.map +1 -1
  121. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTrash.mjs +1 -1
  122. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconTrash.mjs.map +1 -1
  123. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconX.mjs +1 -1
  124. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconX.mjs.map +1 -1
  125. package/dist/types/AddressDisplay/AddressDisplay.d.ts +0 -1
  126. package/dist/types/AddressInput/AddressInput.d.ts +0 -1
  127. package/dist/types/AnnotationInput/AnnotationInput.d.ts +0 -1
  128. package/dist/types/AppShell/AppShell.d.ts +10 -0
  129. package/dist/types/AppShell/Header.d.ts +8 -0
  130. package/dist/types/AppShell/HeaderSearchInput.d.ts +3 -0
  131. package/dist/types/AppShell/Navbar.d.ts +15 -0
  132. package/dist/types/AsyncAutocomplete/AsyncAutocomplete.d.ts +0 -1
  133. package/dist/types/AttachmentArrayDisplay/AttachmentArrayDisplay.d.ts +0 -1
  134. package/dist/types/AttachmentArrayInput/AttachmentArrayInput.d.ts +0 -1
  135. package/dist/types/AttachmentDisplay/AttachmentDisplay.d.ts +0 -1
  136. package/dist/types/AttachmentInput/AttachmentInput.d.ts +0 -1
  137. package/dist/types/BackboneElementDisplay/BackboneElementDisplay.d.ts +0 -1
  138. package/dist/types/BackboneElementInput/BackboneElementInput.d.ts +0 -1
  139. package/dist/types/BookmarkDialog/BookmarkDialog.d.ts +8 -0
  140. package/dist/types/CalendarInput/CalendarInput.d.ts +0 -1
  141. package/dist/types/CodeInput/CodeInput.d.ts +0 -1
  142. package/dist/types/CodeableConceptDisplay/CodeableConceptDisplay.d.ts +0 -1
  143. package/dist/types/CodeableConceptInput/CodeableConceptInput.d.ts +0 -1
  144. package/dist/types/CodingDisplay/CodingDisplay.d.ts +0 -1
  145. package/dist/types/CodingInput/CodingInput.d.ts +0 -1
  146. package/dist/types/ContactDetailDisplay/ContactDetailDisplay.d.ts +0 -1
  147. package/dist/types/ContactDetailInput/ContactDetailInput.d.ts +0 -1
  148. package/dist/types/ContactPointDisplay/ContactPointDisplay.d.ts +0 -1
  149. package/dist/types/ContactPointInput/ContactPointInput.d.ts +0 -1
  150. package/dist/types/Container/Container.d.ts +0 -1
  151. package/dist/types/DateTimeInput/DateTimeInput.d.ts +0 -1
  152. package/dist/types/DefaultResourceTimeline/DefaultResourceTimeline.d.ts +0 -1
  153. package/dist/types/DiagnosticReportDisplay/DiagnosticReportDisplay.d.ts +0 -1
  154. package/dist/types/Document/Document.d.ts +0 -1
  155. package/dist/types/EncounterTimeline/EncounterTimeline.d.ts +0 -1
  156. package/dist/types/ExtensionInput/ExtensionInput.d.ts +0 -1
  157. package/dist/types/FhirPathDisplay/FhirPathDisplay.d.ts +0 -1
  158. package/dist/types/GoogleButton/GoogleButton.d.ts +0 -1
  159. package/dist/types/HumanNameDisplay/HumanNameDisplay.d.ts +0 -1
  160. package/dist/types/HumanNameInput/HumanNameInput.d.ts +0 -1
  161. package/dist/types/IdentifierDisplay/IdentifierDisplay.d.ts +0 -1
  162. package/dist/types/IdentifierInput/IdentifierInput.d.ts +0 -1
  163. package/dist/types/Loading/Loading.d.ts +1 -0
  164. package/dist/types/Logo/Logo.d.ts +0 -1
  165. package/dist/types/MedplumLink/MedplumLink.d.ts +1 -1
  166. package/dist/types/MoneyDisplay/MoneyDisplay.d.ts +0 -1
  167. package/dist/types/MoneyInput/MoneyInput.d.ts +0 -1
  168. package/dist/types/NoteDisplay/NoteDisplay.d.ts +0 -1
  169. package/dist/types/OperationOutcomeAlert/OperationOutcomeAlert.d.ts +0 -1
  170. package/dist/types/Panel/Panel.d.ts +0 -1
  171. package/dist/types/PatientTimeline/PatientTimeline.d.ts +0 -1
  172. package/dist/types/PeriodInput/PeriodInput.d.ts +0 -1
  173. package/dist/types/PlanDefinitionBuilder/PlanDefinitionBuilder.d.ts +0 -1
  174. package/dist/types/QuantityDisplay/QuantityDisplay.d.ts +0 -1
  175. package/dist/types/QuantityInput/QuantityInput.d.ts +0 -1
  176. package/dist/types/QuestionnaireBuilder/QuestionnaireBuilder.d.ts +0 -1
  177. package/dist/types/QuestionnaireForm/QuestionnaireForm.d.ts +0 -1
  178. package/dist/types/RangeDisplay/RangeDisplay.d.ts +0 -1
  179. package/dist/types/RangeInput/RangeInput.d.ts +0 -1
  180. package/dist/types/RatioDisplay/RatioDisplay.d.ts +0 -1
  181. package/dist/types/RatioInput/RatioInput.d.ts +0 -1
  182. package/dist/types/ReferenceDisplay/ReferenceDisplay.d.ts +0 -1
  183. package/dist/types/ReferenceInput/ReferenceInput.d.ts +0 -1
  184. package/dist/types/ReferenceRangeEditor/ReferenceRangeEditor.d.ts +0 -1
  185. package/dist/types/RequestGroupDisplay/RequestGroupDisplay.d.ts +0 -1
  186. package/dist/types/ResourceArrayDisplay/ResourceArrayDisplay.d.ts +0 -1
  187. package/dist/types/ResourceArrayInput/ResourceArrayInput.d.ts +0 -1
  188. package/dist/types/ResourceAvatar/ResourceAvatar.d.ts +0 -1
  189. package/dist/types/ResourceBadge/ResourceBadge.d.ts +0 -1
  190. package/dist/types/ResourceBlame/ResourceBlame.d.ts +0 -1
  191. package/dist/types/ResourceDiff/ResourceDiff.d.ts +0 -1
  192. package/dist/types/ResourceDiffTable/ResourceDiffTable.d.ts +0 -1
  193. package/dist/types/ResourceForm/ResourceForm.d.ts +0 -1
  194. package/dist/types/ResourceHistoryTable/ResourceHistoryTable.d.ts +0 -1
  195. package/dist/types/ResourceInput/ResourceInput.d.ts +0 -1
  196. package/dist/types/ResourceName/ResourceName.d.ts +0 -1
  197. package/dist/types/ResourcePropertyDisplay/ResourcePropertyDisplay.d.ts +0 -1
  198. package/dist/types/ResourcePropertyInput/ResourcePropertyInput.d.ts +0 -1
  199. package/dist/types/ResourceTable/ResourceTable.d.ts +0 -1
  200. package/dist/types/ResourceTimeline/ResourceTimeline.d.ts +0 -1
  201. package/dist/types/Scheduler/Scheduler.d.ts +0 -1
  202. package/dist/types/SearchControl/SearchUtils.d.ts +0 -1
  203. package/dist/types/SearchExportDialog/SearchExportDialog.d.ts +0 -1
  204. package/dist/types/SearchFieldEditor/SearchFieldEditor.d.ts +0 -1
  205. package/dist/types/SearchFilterEditor/SearchFilterEditor.d.ts +0 -1
  206. package/dist/types/SearchFilterValueDialog/SearchFilterValueDialog.d.ts +0 -1
  207. package/dist/types/SearchFilterValueDisplay/SearchFilterValueDisplay.d.ts +0 -1
  208. package/dist/types/SearchFilterValueInput/SearchFilterValueInput.d.ts +0 -1
  209. package/dist/types/SearchPopupMenu/SearchPopupMenu.d.ts +0 -1
  210. package/dist/types/ServiceRequestTimeline/ServiceRequestTimeline.d.ts +0 -1
  211. package/dist/types/StatusBadge/StatusBadge.d.ts +0 -1
  212. package/dist/types/TimingInput/TimingInput.d.ts +0 -1
  213. package/dist/types/ValueSetAutocomplete/ValueSetAutocomplete.d.ts +0 -1
  214. package/dist/types/auth/ChooseProfileForm.d.ts +0 -1
  215. package/dist/types/auth/ChooseScopeForm.d.ts +0 -1
  216. package/dist/types/auth/MfaForm.d.ts +0 -1
  217. package/dist/types/auth/NewProjectForm.d.ts +0 -1
  218. package/dist/types/auth/NewUserForm.d.ts +1 -1
  219. package/dist/types/auth/RegisterForm.d.ts +1 -1
  220. package/dist/types/index.d.ts +7 -3
  221. 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,25 @@
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 IconPlus = createReactComponent("plus", "IconPlus", [
688
+ ["path", { d: "M12 5l0 14", key: "svg-0" }],
689
+ ["path", { d: "M5 12l14 0", key: "svg-1" }]
690
+ ]);
691
+
692
+ /**
693
+ * @tabler/icons-react v2.17.0 - MIT
694
+ */
695
+
696
+ var IconSearch = createReactComponent("search", "IconSearch", [
697
+ ["path", { d: "M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0", key: "svg-0" }],
698
+ ["path", { d: "M21 21l-6 -6", key: "svg-1" }]
699
+ ]);
700
+
701
+ /**
702
+ * @tabler/icons-react v2.17.0 - MIT
824
703
  */
825
704
 
826
705
  var IconSettings = createReactComponent("settings", "IconSettings", [
@@ -835,7 +714,7 @@
835
714
  ]);
836
715
 
837
716
  /**
838
- * @tabler/icons-react v2.16.0 - MIT
717
+ * @tabler/icons-react v2.17.0 - MIT
839
718
  */
840
719
 
841
720
  var IconSortAscending = createReactComponent("sort-ascending", "IconSortAscending", [
@@ -847,7 +726,7 @@
847
726
  ]);
848
727
 
849
728
  /**
850
- * @tabler/icons-react v2.16.0 - MIT
729
+ * @tabler/icons-react v2.17.0 - MIT
851
730
  */
852
731
 
853
732
  var IconSortDescending = createReactComponent("sort-descending", "IconSortDescending", [
@@ -859,7 +738,7 @@
859
738
  ]);
860
739
 
861
740
  /**
862
- * @tabler/icons-react v2.16.0 - MIT
741
+ * @tabler/icons-react v2.17.0 - MIT
863
742
  */
864
743
 
865
744
  var IconSquare = createReactComponent("square", "IconSquare", [
@@ -873,7 +752,22 @@
873
752
  ]);
874
753
 
875
754
  /**
876
- * @tabler/icons-react v2.16.0 - MIT
755
+ * @tabler/icons-react v2.17.0 - MIT
756
+ */
757
+
758
+ var IconSwitchHorizontal = createReactComponent(
759
+ "switch-horizontal",
760
+ "IconSwitchHorizontal",
761
+ [
762
+ ["path", { d: "M16 3l4 4l-4 4", key: "svg-0" }],
763
+ ["path", { d: "M10 7l10 0", key: "svg-1" }],
764
+ ["path", { d: "M8 13l-4 4l4 4", key: "svg-2" }],
765
+ ["path", { d: "M4 17l9 0", key: "svg-3" }]
766
+ ]
767
+ );
768
+
769
+ /**
770
+ * @tabler/icons-react v2.17.0 - MIT
877
771
  */
878
772
 
879
773
  var IconTableExport = createReactComponent("table-export", "IconTableExport", [
@@ -891,7 +785,7 @@
891
785
  ]);
892
786
 
893
787
  /**
894
- * @tabler/icons-react v2.16.0 - MIT
788
+ * @tabler/icons-react v2.17.0 - MIT
895
789
  */
896
790
 
897
791
  var IconTrash = createReactComponent("trash", "IconTrash", [
@@ -906,7 +800,7 @@
906
800
  ]);
907
801
 
908
802
  /**
909
- * @tabler/icons-react v2.16.0 - MIT
803
+ * @tabler/icons-react v2.17.0 - MIT
910
804
  */
911
805
 
912
806
  var IconX = createReactComponent("x", "IconX", [
@@ -914,751 +808,1058 @@
914
808
  ["path", { d: "M6 6l12 12", key: "svg-1" }]
915
809
  ]);
916
810
 
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();
811
+ /**
812
+ * ErrorBoundary is a React component that handles errors in its child components.
813
+ * See: https://reactjs.org/docs/error-boundaries.html
814
+ */
815
+ class ErrorBoundary extends React.Component {
816
+ constructor(props) {
817
+ super(props);
818
+ this.state = {};
923
819
  }
924
- function onFileChange(e) {
925
- killEvent(e);
926
- const files = e.target.files;
927
- if (files) {
928
- Array.from(files).forEach(processFile);
929
- }
820
+ static getDerivedStateFromError(error) {
821
+ return { error };
930
822
  }
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();
823
+ componentDidCatch(error, errorInfo) {
824
+ console.error('Uncaught error:', error, errorInfo);
825
+ }
826
+ render() {
827
+ if (this.state.error) {
828
+ return (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), title: "Something went wrong", color: "red" }, core.normalizeErrorString(this.state.error)));
946
829
  }
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
- });
830
+ return this.props.children;
961
831
  }
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
832
  }
966
833
 
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 })))))))));
834
+ function Loading() {
835
+ return (React.createElement(core$1.Center, { style: { width: '100%', height: '100vh' } },
836
+ React.createElement(core$1.Loader, null)));
1000
837
  }
1001
838
 
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")));
839
+ function HumanNameDisplay(props) {
840
+ const name = props.value;
841
+ if (!name) {
842
+ return null;
1017
843
  }
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)));
844
+ return React.createElement(React.Fragment, null, core.formatHumanName(name, props.options));
1068
845
  }
1069
846
 
1070
847
  /**
1071
- * Parses an HTML form and returns the result as a JavaScript object.
1072
- * @param form The HTML form element.
848
+ * Kills a browser event.
849
+ * Prevents default behavior.
850
+ * Stops event propagation.
851
+ * @param e The event.
1073
852
  */
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;
853
+ function killEvent(e) {
854
+ e.preventDefault();
855
+ e.stopPropagation();
1088
856
  }
1089
857
  /**
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.
858
+ * Returns true if the element is a checkbox or a table cell containing a checkbox.
859
+ * Table cells containing checkboxes are commonly accidentally clicked.
860
+ * @param el The HTML DOM element.
861
+ * @returns True if the element is a checkbox or a table cell containing a checkbox.
1095
862
  */
1096
- function parseInputElement(result, el) {
1097
- if (el.disabled) {
1098
- // Ignore disabled elements
1099
- return;
863
+ function isCheckboxCell(el) {
864
+ if (isCheckboxElement(el)) {
865
+ return true;
1100
866
  }
1101
- if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
1102
- // Ignore unchecked radio or checkbox elements
1103
- return;
867
+ if (el instanceof HTMLTableCellElement) {
868
+ const children = el.children;
869
+ if (children.length === 1 && isCheckboxElement(children[0])) {
870
+ return true;
871
+ }
1104
872
  }
1105
- result[el.name] = el.value;
873
+ return false;
1106
874
  }
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;
875
+ function isCheckboxElement(el) {
876
+ return el instanceof HTMLInputElement && el.type === 'checkbox';
1115
877
  }
1116
878
 
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);
879
+ function MedplumLink(props) {
880
+ const navigate = useMedplumNavigate();
881
+ const { to, suffix, label, onClick, children, ...rest } = props;
882
+ let href = getHref(to);
883
+ if (suffix) {
884
+ href += '/' + suffix;
885
+ }
886
+ return (React.createElement(core$1.Anchor, { href: href, "aria-label": label, onClick: (e) => {
887
+ killEvent(e);
888
+ if (onClick) {
889
+ onClick(e);
1123
890
  }
1124
- } }, props.children));
891
+ else if (to) {
892
+ navigate(href);
893
+ }
894
+ }, ...rest }, children));
1125
895
  }
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" })));
896
+ function getHref(to) {
897
+ if (to) {
898
+ if (typeof to === 'string') {
899
+ return getStringHref(to);
900
+ }
901
+ else if (core.isResource(to)) {
902
+ return getResourceHref(to);
903
+ }
904
+ else if (core.isReference(to)) {
905
+ return getReferenceHref(to);
906
+ }
907
+ }
908
+ return '#';
1135
909
  }
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');
910
+ function getStringHref(to) {
911
+ if (to.startsWith('http://') || to.startsWith('https://') || to.startsWith('/')) {
912
+ return to;
913
+ }
914
+ return '/' + to;
1142
915
  }
1143
- function getIssuesForExpression(outcome, expression) {
1144
- return outcome?.issue?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression));
916
+ function getResourceHref(to) {
917
+ return `/${to.resourceType}/${to.id}`;
1145
918
  }
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;
919
+ function getReferenceHref(to) {
920
+ return `/${to.reference}`;
1163
921
  }
1164
922
 
1165
- function NewProjectForm(props) {
923
+ /**
924
+ * React Hook to use a FHIR reference.
925
+ * Handles the complexity of resolving references and caching resources.
926
+ * @param value The resource or reference to resource.
927
+ * @returns The resolved resource.
928
+ */
929
+ function useResource(value, setOutcome) {
1166
930
  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"))));
931
+ const [resource, setResource] = React.useState(getInitialResource(medplum, value));
932
+ const setResourceIfChanged = React.useCallback((r) => {
933
+ if (!core.deepEquals(r, resource)) {
934
+ setResource(r);
935
+ }
936
+ }, [resource, setResource]);
937
+ React.useEffect(() => {
938
+ setResourceIfChanged(getInitialResource(medplum, value));
939
+ }, [medplum, value, setResourceIfChanged]);
940
+ React.useEffect(() => {
941
+ let subscribed = true;
942
+ if (core.isReference(value)) {
943
+ medplum
944
+ .readReference(value)
945
+ .then((r) => {
946
+ if (subscribed) {
947
+ setResourceIfChanged(r);
948
+ }
949
+ })
950
+ .catch((err) => {
951
+ if (subscribed) {
952
+ setResourceIfChanged(undefined);
953
+ if (setOutcome) {
954
+ setOutcome(core.normalizeOperationOutcome(err));
955
+ }
956
+ }
957
+ });
958
+ }
959
+ return (() => (subscribed = false));
960
+ }, [medplum, resource, value, setResourceIfChanged, setOutcome]);
961
+ return resource;
1193
962
  }
1194
-
1195
963
  /**
1196
- * Dynamically creates a script tag for the specified JavaScript file.
1197
- * @param src The JavaScript file URL.
964
+ * Returns the initial resource value based on the input value.
965
+ * If the input value is a resource, returns the resource.
966
+ * If the input value is a reference to a resource available in the cache, returns the resource.
967
+ * Otherwise, returns undefined.
968
+ * @param medplum The medplum client.
969
+ * @param value The resource or reference to resource.
970
+ * @returns An initial resource if available; undefined otherwise.
1198
971
  */
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);
972
+ function getInitialResource(medplum, value) {
973
+ if (value) {
974
+ if (core.isResource(value)) {
975
+ return value;
976
+ }
977
+ if (core.isReference(value)) {
978
+ return medplum.getCachedReference(value);
979
+ }
980
+ }
981
+ return undefined;
1206
982
  }
1207
983
 
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));
984
+ function ResourceAvatar(props) {
985
+ const resource = useResource(props.value);
986
+ const text = resource ? core.getDisplayString(resource) : props.alt ?? '';
987
+ const imageUrl = (resource && core.getImageSrc(resource)) ?? props.src;
988
+ const radius = props.radius ?? 'xl';
989
+ const avatarProps = { ...props };
990
+ delete avatarProps.value;
991
+ delete avatarProps.link;
992
+ if (props.link) {
993
+ return (React.createElement(MedplumLink, { to: resource },
994
+ React.createElement(core$1.Avatar, { src: imageUrl, alt: text, radius: radius, ...avatarProps })));
995
+ }
996
+ return React.createElement(core$1.Avatar, { src: imageUrl, alt: text, radius: radius, ...avatarProps });
997
+ }
998
+
999
+ function AsyncAutocomplete(props) {
1000
+ const { defaultValue, toKey, toOption, loadOptions, onChange, onCreate, creatable, ...rest } = props;
1001
+ const defaultItems = toDefaultItems(defaultValue);
1002
+ const inputRef = React.useRef(null);
1003
+ const [lastValue, setLastValue] = React.useState(undefined);
1004
+ const [timer, setTimer] = React.useState();
1005
+ const [abortController, setAbortController] = React.useState();
1006
+ const [autoSubmit, setAutoSubmit] = React.useState();
1007
+ const [options, setOptions] = React.useState(defaultItems?.map(toOption));
1008
+ const lastValueRef = React.useRef();
1009
+ lastValueRef.current = lastValue;
1010
+ const timerRef = React.useRef();
1011
+ timerRef.current = timer;
1012
+ const abortControllerRef = React.useRef();
1013
+ abortControllerRef.current = abortController;
1014
+ const autoSubmitRef = React.useRef();
1015
+ autoSubmitRef.current = autoSubmit;
1016
+ const optionsRef = React.useRef();
1017
+ optionsRef.current = options;
1018
+ const handleTimer = React.useCallback(() => {
1019
+ setTimer(undefined);
1020
+ const value = inputRef.current?.value?.trim() || '';
1021
+ if (value === lastValueRef.current) {
1022
+ // Nothing has changed, move on
1218
1023
  return;
1219
1024
  }
1220
- if (!initialized) {
1221
- google.accounts.id.initialize({
1222
- client_id: googleClientId,
1223
- callback: handleGoogleCredential,
1224
- });
1225
- setInitialized(true);
1025
+ setLastValue(value);
1026
+ const newAbortController = new AbortController();
1027
+ setAbortController(newAbortController);
1028
+ loadOptions(value, newAbortController.signal)
1029
+ .then((newValues) => {
1030
+ if (!newAbortController.signal.aborted) {
1031
+ setOptions(newValues.map(toOption));
1032
+ setAbortController(undefined);
1033
+ if (autoSubmitRef.current) {
1034
+ if (newValues.length > 0) {
1035
+ onChange(newValues.slice(0, 1));
1036
+ }
1037
+ setAutoSubmit(false);
1038
+ }
1039
+ }
1040
+ })
1041
+ .catch(console.log);
1042
+ }, [loadOptions, onChange, toOption]);
1043
+ const handleSearchChange = React.useCallback(() => {
1044
+ if (abortControllerRef.current) {
1045
+ abortControllerRef.current.abort();
1046
+ setAbortController(undefined);
1226
1047
  }
1227
- if (parentRef.current && !buttonRendered) {
1228
- google.accounts.id.renderButton(parentRef.current, {});
1229
- setButtonRendered(true);
1048
+ if (timerRef.current !== undefined) {
1049
+ window.clearTimeout(timerRef.current);
1230
1050
  }
1231
- }, [medplum, googleClientId, initialized, scriptLoaded, parentRef, buttonRendered, handleGoogleCredential]);
1232
- if (!googleClientId) {
1233
- return null;
1234
- }
1235
- return React.createElement("div", { ref: parentRef });
1051
+ const newTimer = window.setTimeout(() => handleTimer(), 100);
1052
+ setTimer(newTimer);
1053
+ }, [handleTimer]);
1054
+ const handleChange = React.useCallback((values) => {
1055
+ const result = [];
1056
+ for (const value of values) {
1057
+ let item = optionsRef.current?.find((option) => option.value === value)?.resource;
1058
+ if (!item && creatable !== false) {
1059
+ item = onCreate(value);
1060
+ }
1061
+ if (item)
1062
+ result.push(item);
1063
+ }
1064
+ onChange(result);
1065
+ }, [creatable, onChange, onCreate]);
1066
+ const handleKeyDown = React.useCallback((e) => {
1067
+ if (e.key === 'Enter') {
1068
+ if (!timerRef.current && !abortControllerRef.current) {
1069
+ killEvent(e);
1070
+ if (optionsRef.current && optionsRef.current.length > 0) {
1071
+ setOptions(optionsRef.current.slice(0, 1));
1072
+ handleChange([optionsRef.current[0].value]);
1073
+ }
1074
+ }
1075
+ else {
1076
+ // The user pressed enter, but we don't have results yet.
1077
+ // We need to wait for the results to come in.
1078
+ setAutoSubmit(true);
1079
+ }
1080
+ }
1081
+ }, [handleChange]);
1082
+ const handleCreate = React.useCallback((input) => {
1083
+ const option = toOption(onCreate(input));
1084
+ setOptions([...optionsRef.current, option]);
1085
+ return option;
1086
+ }, [onCreate, setOptions, toOption]);
1087
+ const handleFilter = React.useCallback((_value, selected) => !selected, []);
1088
+ React.useEffect(() => {
1089
+ return () => {
1090
+ if (abortControllerRef.current) {
1091
+ abortControllerRef.current.abort();
1092
+ }
1093
+ };
1094
+ }, []);
1095
+ 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
1096
  }
1237
- function getGoogleClientId(clientId) {
1238
- if (clientId) {
1239
- return clientId;
1097
+ function toDefaultItems(defaultValue) {
1098
+ if (!defaultValue) {
1099
+ return [];
1240
1100
  }
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
- }
1101
+ if (Array.isArray(defaultValue)) {
1102
+ return defaultValue;
1247
1103
  }
1248
- return undefined;
1104
+ return [defaultValue];
1249
1105
  }
1250
1106
 
1251
- function OperationOutcomeAlert(props) {
1252
- const issues = props.outcome?.issue || props.issues;
1253
- if (!issues) {
1254
- return null;
1107
+ const useStyles$h = core$1.createStyles(() => {
1108
+ return {
1109
+ searchInput: {
1110
+ input: {
1111
+ width: 220,
1112
+ transition: 'width 0.2s',
1113
+ },
1114
+ 'input:focus': {
1115
+ width: 400,
1116
+ },
1117
+ '@media (max-width: 800px)': {
1118
+ input: {
1119
+ width: 150,
1120
+ },
1121
+ 'input:focus': {
1122
+ width: 150,
1123
+ },
1124
+ },
1125
+ },
1126
+ };
1127
+ });
1128
+ function toKey$1(resource) {
1129
+ return resource.id;
1130
+ }
1131
+ function toOption$1(resource) {
1132
+ return {
1133
+ value: resource.id,
1134
+ label: core.getDisplayString(resource),
1135
+ resource,
1136
+ };
1137
+ }
1138
+ function HeaderSearchInput() {
1139
+ const { classes } = useStyles$h();
1140
+ const navigate = useMedplumNavigate();
1141
+ const medplum = useMedplum();
1142
+ const location = reactRouterDom.useLocation();
1143
+ const loadData = React.useCallback(async (input, signal) => {
1144
+ const query = buildGraphQLQuery(input);
1145
+ const options = { signal };
1146
+ const response = (await medplum.graphql(query, undefined, undefined, options));
1147
+ return getResourcesFromResponse(response, input);
1148
+ }, [medplum]);
1149
+ const handleSelect = React.useCallback((item) => {
1150
+ if (item.length > 0) {
1151
+ navigate(`/${core.getReferenceString(item[0])}`);
1152
+ }
1153
+ }, [navigate]);
1154
+ 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 }));
1155
+ }
1156
+ const ItemComponent$1 = React.forwardRef(({ resource, ...others }, ref) => {
1157
+ let helpText = undefined;
1158
+ if (resource.resourceType === 'Patient') {
1159
+ helpText = resource.birthDate;
1255
1160
  }
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)))));
1161
+ else if (resource.resourceType === 'ServiceRequest') {
1162
+ helpText = resource.subject?.display;
1163
+ }
1164
+ return (React.createElement("div", { ref: ref, ...others },
1165
+ React.createElement(core$1.Group, { noWrap: true },
1166
+ React.createElement(ResourceAvatar, { value: resource }),
1167
+ React.createElement("div", null,
1168
+ React.createElement(core$1.Text, null, core.getDisplayString(resource)),
1169
+ React.createElement(core$1.Text, { size: "xs", color: "dimmed" }, helpText)))));
1170
+ });
1171
+ function buildGraphQLQuery(input) {
1172
+ const escaped = JSON.stringify(input);
1173
+ if (core.isUUID(input)) {
1174
+ return `{
1175
+ Patients1: PatientList(_id: ${escaped}, _count: 1) {
1176
+ resourceType
1177
+ id
1178
+ identifier {
1179
+ system
1180
+ value
1181
+ }
1182
+ name {
1183
+ given
1184
+ family
1185
+ }
1186
+ birthDate
1187
+ }
1188
+ ServiceRequestList(_id: ${escaped}, _count: 1) {
1189
+ resourceType
1190
+ id
1191
+ identifier {
1192
+ system
1193
+ value
1194
+ }
1195
+ subject {
1196
+ display
1197
+ }
1198
+ }
1199
+ }`.replace(/\s+/g, ' ');
1200
+ }
1201
+ return `{
1202
+ Patients1: PatientList(name: ${escaped}, _count: 5) {
1203
+ resourceType
1204
+ id
1205
+ identifier {
1206
+ system
1207
+ value
1208
+ }
1209
+ name {
1210
+ given
1211
+ family
1212
+ }
1213
+ birthDate
1214
+ }
1215
+ Patients2: PatientList(identifier: ${escaped}, _count: 5) {
1216
+ resourceType
1217
+ id
1218
+ identifier {
1219
+ system
1220
+ value
1221
+ }
1222
+ name {
1223
+ given
1224
+ family
1225
+ }
1226
+ birthDate
1227
+ }
1228
+ ServiceRequestList(identifier: ${escaped}, _count: 5) {
1229
+ resourceType
1230
+ id
1231
+ identifier {
1232
+ system
1233
+ value
1234
+ }
1235
+ subject {
1236
+ display
1237
+ }
1238
+ }
1239
+ }`.replace(/\s+/g, ' ');
1257
1240
  }
1258
-
1259
1241
  /**
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.
1242
+ * Returns a de-duped and sorted list of resources from the search response.
1243
+ * The search request is actually 3+ separate searches, which can include duplicates.
1244
+ * This function combines the results, de-dupes, and sorts by relevance.
1245
+ * @param response The response from a search query.
1246
+ * @param query The user entered search query.
1247
+ * @returns The resources to display in the autocomplete.
1263
1248
  */
1264
- function initRecaptcha(siteKey) {
1265
- if (typeof grecaptcha === 'undefined') {
1266
- createScriptTag('https://www.google.com/recaptcha/api.js?render=' + siteKey);
1249
+ function getResourcesFromResponse(response, query) {
1250
+ const resources = [];
1251
+ if (response.data.Patients1) {
1252
+ resources.push(...response.data.Patients1);
1253
+ }
1254
+ if (response.data.Patients2) {
1255
+ resources.push(...response.data.Patients2);
1256
+ }
1257
+ if (response.data.ServiceRequestList) {
1258
+ resources.push(...response.data.ServiceRequestList);
1267
1259
  }
1260
+ return sortByRelevance(dedupeResources(resources), query).slice(0, 5);
1268
1261
  }
1269
1262
  /**
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.
1263
+ * Removes duplicate resources from an array by ID.
1264
+ * @param resources The array of resources with possible duplicates.
1265
+ * @returns The array of resources with no duplicates.
1273
1266
  */
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
- });
1267
+ function dedupeResources(resources) {
1268
+ const ids = new Set();
1269
+ const result = [];
1270
+ for (const resource of resources) {
1271
+ if (!ids.has(resource.id)) {
1272
+ ids.add(resource.id);
1273
+ result.push(resource);
1274
+ }
1275
+ }
1276
+ return result;
1277
+ }
1278
+ /**
1279
+ * Sorts an array of resources by relevance.
1280
+ * @param resources The candidate resources.
1281
+ * @param query The user entered search string.
1282
+ * @returns The sorted array of resources.
1283
+ */
1284
+ function sortByRelevance(resources, query) {
1285
+ return resources.sort((a, b) => {
1286
+ return getResourceScore(b, query) - getResourceScore(a, query);
1284
1287
  });
1285
1288
  }
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);
1289
+ /**
1290
+ * Calculates a relevance score of a candidate resource.
1291
+ * Higher scores are better.
1292
+ * @param resource The candidate resource.
1293
+ * @param query The user entered search string.
1294
+ * @returns The relevance score of the candidate resource.
1295
+ */
1296
+ function getResourceScore(resource, query) {
1297
+ let bestScore = 0;
1298
+ if (resource.identifier) {
1299
+ for (const identifier of resource.identifier) {
1300
+ bestScore = Math.max(bestScore, getStringScore(identifier.value, query));
1296
1301
  }
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"))));
1302
+ }
1303
+ if (resource.resourceType === 'Patient' && resource.name) {
1304
+ for (const name of resource.name) {
1305
+ bestScore = Math.max(bestScore, getStringScore(core.formatHumanName(name), query));
1306
+ }
1307
+ }
1308
+ return bestScore;
1309
+ }
1310
+ /**
1311
+ * Calculates a relevance score of a candidate display string.
1312
+ * Higher scores are better.
1313
+ * @param str The candidate display string.
1314
+ * @param query The user entered search string.
1315
+ * @returns The relevance score of the candidate string.
1316
+ */
1317
+ function getStringScore(str, query) {
1318
+ if (!str) {
1319
+ return 0;
1320
+ }
1321
+ const index = str.toLowerCase().indexOf(query.toLowerCase());
1322
+ if (index < 0) {
1323
+ return 0;
1324
+ }
1325
+ return 100 - index;
1358
1326
  }
1359
1327
 
1360
- function RegisterForm(props) {
1361
- const { type, projectId, googleClientId, recaptchaSiteKey, onSuccess } = props;
1362
- 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));
1328
+ const useStyles$g = core$1.createStyles((theme) => ({
1329
+ logoButton: {
1330
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1331
+ borderRadius: theme.radius.sm,
1332
+ transition: 'background-color 100ms ease',
1333
+ '&:hover': {
1334
+ backgroundColor: theme.fn.lighten(theme.fn.variant({ variant: 'filled', color: theme.primaryColor }).background, 0.8),
1335
+ },
1336
+ },
1337
+ user: {
1338
+ padding: `${theme.spacing.xs} ${theme.spacing.sm}`,
1339
+ borderRadius: theme.radius.sm,
1340
+ transition: 'background-color 100ms ease',
1341
+ '&:hover': {
1342
+ backgroundColor: theme.fn.lighten(theme.fn.variant({ variant: 'filled', color: theme.primaryColor }).background, 0.8),
1343
+ },
1344
+ },
1345
+ userName: {
1346
+ fontWeight: 500,
1347
+ lineHeight: 1,
1348
+ marginRight: 3,
1349
+ [theme.fn.smallerThan('xs')]: {
1350
+ display: 'none',
1351
+ },
1352
+ },
1353
+ userActive: {
1354
+ backgroundColor: theme.fn.lighten(theme.fn.variant({ variant: 'filled', color: theme.primaryColor }).background, 0.8),
1355
+ },
1356
+ }));
1357
+ function Header(props) {
1358
+ const context = useMedplumContext();
1359
+ const { medplum, profile, navigate } = context;
1360
+ const logins = medplum.getLogins();
1361
+ const { classes, cx } = useStyles$g();
1362
+ const [userMenuOpened, setUserMenuOpened] = React.useState(false);
1363
+ return (React.createElement(core$1.Header, { height: 60, p: 8, style: { zIndex: 101 } },
1364
+ React.createElement(core$1.Group, { position: "apart" },
1365
+ React.createElement(core$1.Group, { spacing: "xs" },
1366
+ React.createElement(core$1.UnstyledButton, { className: classes.logoButton, onClick: props.navbarToggle }, props.logo),
1367
+ React.createElement(HeaderSearchInput, null)),
1368
+ React.createElement(core$1.Menu, { width: 260, shadow: "xl", position: "bottom-end", transitionProps: { transition: 'pop-top-right' }, opened: userMenuOpened, onClose: () => setUserMenuOpened(false) },
1369
+ React.createElement(core$1.Menu.Target, null,
1370
+ React.createElement(core$1.UnstyledButton, { className: cx(classes.user, { [classes.userActive]: userMenuOpened }), onClick: () => setUserMenuOpened((o) => !o) },
1371
+ React.createElement(core$1.Group, { spacing: 7 },
1372
+ React.createElement(ResourceAvatar, { value: profile, radius: "xl", size: 24 }),
1373
+ React.createElement(core$1.Text, { size: "sm", className: classes.userName }, core.formatHumanName(profile?.name?.[0])),
1374
+ React.createElement(IconChevronDown, { size: 12, stroke: 1.5 })))),
1375
+ React.createElement(core$1.Menu.Dropdown, null,
1376
+ React.createElement(core$1.Stack, { align: "center", p: "xl" },
1377
+ React.createElement(ResourceAvatar, { size: "xl", radius: 100, value: context.profile }),
1378
+ React.createElement(HumanNameDisplay, { value: context.profile?.name?.[0] }),
1379
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, medplum.getActiveLogin()?.project?.display)),
1380
+ logins.length > 1 && React.createElement(core$1.Menu.Divider, null),
1381
+ logins.map((login) => login.profile?.reference !== core.getReferenceString(context.profile) && (React.createElement(core$1.Menu.Item, { key: login.profile?.reference, onClick: () => {
1382
+ medplum
1383
+ .setActiveLogin(login)
1384
+ .then(() => window.location.reload())
1385
+ .catch(console.log);
1386
+ } },
1387
+ React.createElement(core$1.Group, null,
1388
+ React.createElement(core$1.Avatar, { radius: "xl" }),
1389
+ React.createElement("div", { style: { flex: 1 } },
1390
+ React.createElement(core$1.Text, { size: "sm", weight: 500 }, login.profile?.display),
1391
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, login.project?.display)))))),
1392
+ React.createElement(core$1.Menu.Divider, null),
1393
+ React.createElement(core$1.Menu.Item, { icon: React.createElement(IconSwitchHorizontal, { size: 14, stroke: 1.5 }), onClick: () => navigate('/signin') }, "Add another account"),
1394
+ React.createElement(core$1.Menu.Item, { icon: React.createElement(IconSettings, { size: 14, stroke: 1.5 }), onClick: () => navigate(`/${core.getReferenceString(profile)}`) }, "Account settings"),
1395
+ React.createElement(core$1.Menu.Item, { icon: React.createElement(IconLogout, { size: 14, stroke: 1.5 }), onClick: async () => {
1396
+ await medplum.signOut();
1397
+ navigate('/signin');
1398
+ } }, "Sign out"),
1399
+ React.createElement(core$1.Text, { size: "xs", color: "dimmed", align: "center" }, props.version))))));
1400
+ }
1401
+
1402
+ /**
1403
+ * Parses an HTML form and returns the result as a JavaScript object.
1404
+ * @param form The HTML form element.
1405
+ */
1406
+ function parseForm(form) {
1407
+ const result = {};
1408
+ for (const element of Array.from(form.elements)) {
1409
+ if (element instanceof HTMLInputElement) {
1410
+ parseInputElement(result, element);
1372
1411
  }
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);
1412
+ else if (element instanceof HTMLTextAreaElement) {
1413
+ result[element.name] = element.value;
1380
1414
  }
1381
- else if (response.login) {
1382
- setLogin(response.login);
1415
+ else if (element instanceof HTMLSelectElement) {
1416
+ parseSelectElement(result, element);
1383
1417
  }
1384
1418
  }
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 })));
1419
+ return result;
1389
1420
  }
1390
-
1391
- function AuthenticationForm(props) {
1392
- const [email, setEmail] = React.useState();
1393
- if (!email) {
1394
- return React.createElement(EmailForm, { setEmail: setEmail, ...props });
1421
+ /**
1422
+ * Parses an HTML input element.
1423
+ * Sets the name/value pair in the result,
1424
+ * but only if the element is enabled and checked.
1425
+ * @param el The input element.
1426
+ * @param result The result builder.
1427
+ */
1428
+ function parseInputElement(result, el) {
1429
+ if (el.disabled) {
1430
+ // Ignore disabled elements
1431
+ return;
1395
1432
  }
1396
- else {
1397
- return React.createElement(PasswordForm, { email: email, ...props });
1433
+ if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
1434
+ // Ignore unchecked radio or checkbox elements
1435
+ return;
1398
1436
  }
1437
+ result[el.name] = el.value;
1399
1438
  }
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) {
1406
- return false;
1407
- }
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"))));
1439
+ /**
1440
+ * Parses an HTML select element.
1441
+ * Sets the name/value pair if one is selected.
1442
+ * @param result The result builder.
1443
+ * @param el The select element.
1444
+ */
1445
+ function parseSelectElement(result, el) {
1446
+ result[el.name] = el.value;
1442
1447
  }
1443
- function PasswordForm(props) {
1444
- const { onForgotPassword, handleAuthResponse, children, ...baseLoginRequest } = props;
1448
+
1449
+ function Form(props) {
1450
+ return (React.createElement("form", { style: props.style, "data-testid": props.testid, onSubmit: (e) => {
1451
+ e.preventDefault();
1452
+ const formData = parseForm(e.target);
1453
+ if (props.onSubmit) {
1454
+ props.onSubmit(formData);
1455
+ }
1456
+ } }, props.children));
1457
+ }
1458
+
1459
+ function BookmarkDialog(props) {
1445
1460
  const medplum = useMedplum();
1446
- const [outcome, setOutcome] = React.useState();
1447
- const issues = getIssuesForExpression(outcome, undefined);
1448
- const handleSubmit = React.useCallback((formData) => {
1461
+ const config = medplum.getUserConfiguration();
1462
+ const location = reactRouterDom.useLocation();
1463
+ function submitHandler(formData) {
1464
+ const { menuname, bookmarkname: name } = formData;
1465
+ const target = location.pathname + location.search;
1466
+ const newConfig = core.deepClone(config);
1467
+ const menu = newConfig?.menu?.find(({ title }) => title === menuname);
1468
+ menu?.link?.push({ name, target });
1449
1469
  medplum
1450
- .startLogin({
1451
- ...baseLoginRequest,
1452
- password: formData.password,
1453
- remember: formData.remember === 'on',
1470
+ .updateResource(newConfig)
1471
+ .then((res) => {
1472
+ // refresh current config menu
1473
+ config.menu = res.menu;
1474
+ medplum.dispatchEvent({ type: 'change' });
1475
+ notifications.showNotification({ color: 'green', message: 'Success' });
1476
+ props.onOk();
1454
1477
  })
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"))));
1478
+ .catch((err) => {
1479
+ notifications.showNotification({ color: 'red', message: core.normalizeErrorString(err) });
1480
+ });
1481
+ }
1482
+ return (React.createElement(core$1.Modal, { title: "Add Bookmark", closeButtonProps: { 'aria-label': 'Close' }, opened: props.visible, onClose: props.onCancel },
1483
+ React.createElement(Form, { onSubmit: submitHandler },
1484
+ React.createElement(core$1.Stack, null,
1485
+ React.createElement(SelectMenu, { config: config }),
1486
+ React.createElement(core$1.TextInput, { label: "Bookmark Name", type: "text", name: "bookmarkname", placeholder: "bookmark name", withAsterisk: true }),
1487
+ React.createElement(core$1.Group, { position: "right" },
1488
+ React.createElement(core$1.Button, { mt: "sm", type: "submit" }, "OK"))))));
1467
1489
  }
1468
-
1469
- function ChooseProfileForm(props) {
1470
- 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))))))));
1490
+ function SelectMenu(props) {
1491
+ function userConfigToMenu(config) {
1492
+ return config?.menu?.map((menu) => menu.title);
1493
+ }
1494
+ const menus = userConfigToMenu(props.config);
1495
+ return (React.createElement(core$1.NativeSelect, { name: "menuname", defaultValue: menus?.[0], label: "Select Menu Option", placeholder: "Menu", data: menus, withAsterisk: true }));
1491
1496
  }
1492
1497
 
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")))));
1498
+ function toKey(element) {
1499
+ return element.code;
1511
1500
  }
1512
-
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")))));
1501
+ function toOption(element) {
1502
+ return {
1503
+ value: element.code,
1504
+ label: getDisplay(element),
1505
+ resource: element,
1506
+ };
1507
+ }
1508
+ function createValue(input) {
1509
+ return {
1510
+ code: input,
1511
+ display: input,
1512
+ };
1535
1513
  }
1536
-
1537
1514
  /**
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
1515
+ * A low-level component to autocomplete based on a FHIR Valueset.
1546
1516
  */
1547
- function SignInForm(props) {
1548
- const { chooseScopes, onSuccess, onForgotPassword, onRegister, onCode, ...baseLoginRequest } = props;
1517
+ function ValueSetAutocomplete(props) {
1549
1518
  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);
1519
+ const { elementDefinition, creatable, clearable, ...rest } = props;
1520
+ const loadValues = React.useCallback(async (input, signal) => {
1521
+ const system = elementDefinition.binding?.valueSet;
1522
+ const valueSet = await medplum.searchValueSet(system, input, { signal });
1523
+ const valueSetElements = valueSet.expansion?.contains;
1524
+ const newData = [];
1525
+ for (const valueSetElement of valueSetElements) {
1526
+ if (valueSetElement.code && !newData.some((item) => item.code === valueSetElement.code)) {
1527
+ newData.push(valueSetElement);
1582
1528
  }
1583
1529
  }
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 });
1605
- }
1606
- else if (props.projectId === 'new') {
1607
- return React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse });
1608
- }
1609
- else if (props.chooseScopes) {
1610
- return React.createElement(ChooseScopeForm, { login: login, scope: props.scope, handleAuthResponse: handleScopeResponse });
1611
- }
1612
- else {
1613
- return React.createElement("div", null, "Success");
1614
- }
1615
- })()));
1530
+ return newData;
1531
+ }, [medplum, elementDefinition]);
1532
+ 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}` }));
1533
+ }
1534
+ function getDisplay(item) {
1535
+ return item.display || item.code || '';
1616
1536
  }
1617
1537
 
1618
- const DEFAULT_IGNORED_PROPERTIES = [
1619
- 'meta',
1620
- 'implicitRules',
1621
- 'language',
1622
- 'text',
1623
- 'contained',
1624
- 'extension',
1625
- 'modifierExtension',
1626
- ];
1538
+ function CodeInput(props) {
1539
+ const [value, setValue] = React.useState(props.defaultValue);
1540
+ function handleChange(newValues) {
1541
+ const newValue = newValues[0];
1542
+ const newCode = valueSetElementToCode(newValue);
1543
+ setValue(newCode);
1544
+ if (props.onChange) {
1545
+ props.onChange(newCode);
1546
+ }
1547
+ }
1548
+ 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 }));
1549
+ }
1550
+ function codeToValueSetElement(code) {
1551
+ return code ? { code } : undefined;
1552
+ }
1553
+ function valueSetElementToCode(element) {
1554
+ return element?.code;
1555
+ }
1627
1556
 
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,
1557
+ const useStyles$f = core$1.createStyles((theme) => {
1558
+ return {
1559
+ menuTitle: {
1560
+ margin: '20px 0 4px 6px',
1561
+ fontSize: '9px',
1562
+ fontWeight: 'normal',
1563
+ textTransform: 'uppercase',
1564
+ letterSpacing: '2px',
1637
1565
  },
1638
- },
1639
- compact: {
1640
- gridTemplateColumns: '20% 80%',
1641
- '& > dt, & > dd': {
1642
- padding: `0 ${theme.spacing.xs} ${theme.spacing.xs} 0`,
1643
- border: 0,
1566
+ link: {
1567
+ ...theme.fn.focusStyles(),
1568
+ display: 'flex',
1569
+ alignItems: 'center',
1570
+ textDecoration: 'none',
1571
+ fontSize: theme.fontSizes.sm,
1572
+ color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[7],
1573
+ padding: `8px 12px`,
1574
+ borderRadius: theme.radius.sm,
1575
+ fontWeight: 500,
1576
+ '&:hover': {
1577
+ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
1578
+ color: theme.colorScheme === 'dark' ? theme.white : theme.black,
1579
+ textDecoration: 'none',
1580
+ [`& svg`]: {
1581
+ color: theme.colorScheme === 'dark' ? theme.white : theme.black,
1582
+ },
1583
+ },
1584
+ '& svg': {
1585
+ color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
1586
+ marginRight: theme.spacing.sm,
1587
+ strokeWidth: 1.5,
1588
+ width: 18,
1589
+ height: 18,
1590
+ },
1644
1591
  },
1645
- },
1646
- }));
1647
- function DescriptionList(props) {
1648
- const { children, compact } = props;
1649
- const { classes, cx } = useStyles$c();
1650
- return React.createElement("dl", { className: cx(classes.root, { [classes.compact]: compact }) }, children);
1651
- }
1652
- function DescriptionListEntry(props) {
1592
+ linkActive: {
1593
+ '&, &:hover': {
1594
+ backgroundColor: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).background,
1595
+ color: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).color,
1596
+ [`& svg`]: {
1597
+ color: theme.fn.variant({ variant: 'light', color: theme.primaryColor }).color,
1598
+ },
1599
+ },
1600
+ },
1601
+ };
1602
+ });
1603
+ function Navbar(props) {
1604
+ const { classes } = useStyles$f();
1605
+ const navigate = useMedplumNavigate();
1606
+ const [bookmarkDialogVisible, setBookmarkDialogVisible] = React.useState(false);
1607
+ function onLinkClick(e, to) {
1608
+ e.stopPropagation();
1609
+ e.preventDefault();
1610
+ navigate(to);
1611
+ if (window.innerWidth < 768) {
1612
+ props.closeNavbar();
1613
+ }
1614
+ }
1615
+ function navigateResourceType(resourceType) {
1616
+ if (resourceType) {
1617
+ navigate(`/${resourceType}`);
1618
+ }
1619
+ }
1653
1620
  return (React.createElement(React.Fragment, null,
1654
- React.createElement("dt", null, props.term),
1655
- React.createElement("dd", null, props.children)));
1656
- }
1657
-
1658
- function CodeableConceptDisplay(props) {
1659
- return React.createElement(React.Fragment, null, core.formatCodeableConcept(props.value));
1621
+ React.createElement(core$1.Navbar, { width: { sm: 250 }, p: "xs" },
1622
+ React.createElement(core$1.Navbar.Section, { mb: "sm" },
1623
+ React.createElement(CodeInput, { key: window.location.pathname, name: "resourceType", placeholder: "Resource Type", property: {
1624
+ binding: {
1625
+ valueSet: 'http://hl7.org/fhir/ValueSet/resource-types',
1626
+ },
1627
+ }, onChange: (newValue) => navigateResourceType(newValue), creatable: false, maxSelectedValues: 0, clearSearchOnChange: true, clearable: false })),
1628
+ React.createElement(core$1.Navbar.Section, { grow: true },
1629
+ props.menus?.map((menu) => (React.createElement(React.Fragment, { key: `menu-${menu.title}` },
1630
+ React.createElement(core$1.Text, { className: classes.menuTitle }, menu.title),
1631
+ menu.links?.map((link) => (React.createElement(NavbarLink, { key: link.href, to: link.href, onClick: (e) => onLinkClick(e, link.href) },
1632
+ React.createElement(NavLinkIcon, { to: link.href, icon: link.icon }),
1633
+ React.createElement("span", null, link.label))))))),
1634
+ props.displayAddBookmark && (React.createElement(core$1.Button, { variant: "subtle", size: "xs", mt: "xl", leftIcon: React.createElement(IconPlus, { size: "0.75rem" }), onClick: () => setBookmarkDialogVisible(true) }, "Add Bookmark")))),
1635
+ React.createElement(BookmarkDialog, { visible: bookmarkDialogVisible, onOk: () => setBookmarkDialogVisible(false), onCancel: () => setBookmarkDialogVisible(false) })));
1636
+ }
1637
+ function NavbarLink(props) {
1638
+ const { classes, cx } = useStyles$f();
1639
+ const location = reactRouterDom.useLocation();
1640
+ const [searchParams] = reactRouterDom.useSearchParams();
1641
+ const toUrl = new URL(props.to, window.location.protocol + '//' + window.location.host);
1642
+ const isActive = location.pathname === toUrl.pathname && matchesParams(searchParams, toUrl);
1643
+ return (React.createElement(MedplumLink, { onClick: props.onClick, to: props.to, className: cx(classes.link, { [classes.linkActive]: isActive }) }, props.children));
1660
1644
  }
1661
-
1645
+ /**
1646
+ * Returns true if the search params match.
1647
+ * @param searchParams The current search params.
1648
+ * @param toUrl The destination URL of the link.
1649
+ * @returns True if the search params match.
1650
+ */
1651
+ function matchesParams(searchParams, toUrl) {
1652
+ for (const [key, value] of toUrl.searchParams.entries()) {
1653
+ if (searchParams.get(key) !== value) {
1654
+ return false;
1655
+ }
1656
+ }
1657
+ return true;
1658
+ }
1659
+ function NavLinkIcon(props) {
1660
+ if (props.icon) {
1661
+ return props.icon;
1662
+ }
1663
+ return React.createElement(core$1.Space, { w: 30 });
1664
+ }
1665
+
1666
+ function AppShell(props) {
1667
+ const theme = core$1.useMantineTheme();
1668
+ const [navbarOpen, setNavbarOpen] = React.useState(localStorage['navbarOpen'] === 'true');
1669
+ const medplum = useMedplum();
1670
+ const profile = useMedplumProfile();
1671
+ function setNavbarOpenWrapper(open) {
1672
+ localStorage['navbarOpen'] = open.toString();
1673
+ setNavbarOpen(open);
1674
+ }
1675
+ function closeNavbar() {
1676
+ setNavbarOpenWrapper(false);
1677
+ }
1678
+ function toggleNavbar() {
1679
+ setNavbarOpenWrapper(!navbarOpen);
1680
+ }
1681
+ if (medplum.isLoading()) {
1682
+ return React.createElement(Loading, null);
1683
+ }
1684
+ return (React.createElement(core$1.AppShell, { styles: {
1685
+ main: {
1686
+ background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
1687
+ },
1688
+ }, 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, displayAddBookmark: props.displayAddBookmark })) : undefined },
1689
+ React.createElement(ErrorBoundary, null,
1690
+ React.createElement(React.Suspense, { fallback: React.createElement(Loading, null) }, props.children))));
1691
+ }
1692
+
1693
+ function AttachmentDisplay(props) {
1694
+ const value = props.value;
1695
+ const { contentType, url, title } = value ?? {};
1696
+ if (!url) {
1697
+ return null;
1698
+ }
1699
+ return (React.createElement("div", { "data-testid": "attachment-display" },
1700
+ contentType?.startsWith('image/') && (React.createElement("img", { "data-testid": "attachment-image", style: { maxWidth: props.maxWidth }, src: url, alt: value?.title })),
1701
+ contentType?.startsWith('video/') && (React.createElement("video", { "data-testid": "attachment-video", style: { maxWidth: props.maxWidth }, controls: true },
1702
+ React.createElement("source", { type: contentType, src: url }))),
1703
+ contentType === 'application/pdf' && !title?.endsWith('.pdf') && (React.createElement("div", { "data-testid": "attachment-pdf", style: { maxWidth: props.maxWidth, minHeight: 400 } },
1704
+ React.createElement("iframe", { width: "100%", height: "400", src: url + '#navpanes=0', allowFullScreen: true, frameBorder: 0, seamless: true }))),
1705
+ React.createElement("div", { "data-testid": "download-link", style: { padding: '2px 16px 16px 16px' } },
1706
+ React.createElement(core$1.Anchor, { href: value?.url, "data-testid": "attachment-details", target: "_blank", rel: "noopener noreferrer" }, value?.title || 'Download'))));
1707
+ }
1708
+
1709
+ function AttachmentArrayDisplay(props) {
1710
+ return (React.createElement("div", null, props.values &&
1711
+ props.values.map((v, index) => (React.createElement("div", { key: 'attatchment-' + index },
1712
+ React.createElement(AttachmentDisplay, { value: v, maxWidth: props.maxWidth }))))));
1713
+ }
1714
+
1715
+ function AttachmentButton(props) {
1716
+ const medplum = useMedplum();
1717
+ const fileInputRef = React.useRef(null);
1718
+ function onClick(e) {
1719
+ killEvent(e);
1720
+ fileInputRef.current?.click();
1721
+ }
1722
+ function onFileChange(e) {
1723
+ killEvent(e);
1724
+ const files = e.target.files;
1725
+ if (files) {
1726
+ Array.from(files).forEach(processFile);
1727
+ }
1728
+ }
1729
+ /**
1730
+ * Processes a single file.
1731
+ *
1732
+ * @param {File} file The file descriptor.
1733
+ */
1734
+ function processFile(file) {
1735
+ if (!file) {
1736
+ return;
1737
+ }
1738
+ const fileName = file.name;
1739
+ if (!fileName) {
1740
+ return;
1741
+ }
1742
+ if (props.onUploadStart) {
1743
+ props.onUploadStart();
1744
+ }
1745
+ const filename = file.name;
1746
+ const contentType = file.type || 'application/octet-stream';
1747
+ medplum
1748
+ .createBinary(file, filename, contentType, props.onUploadProgress)
1749
+ .then((binary) => {
1750
+ props.onUpload({
1751
+ contentType: binary.contentType,
1752
+ url: binary.url,
1753
+ title: filename,
1754
+ });
1755
+ })
1756
+ .catch((outcome) => {
1757
+ alert(outcome?.issue?.[0]?.details?.text);
1758
+ });
1759
+ }
1760
+ return (React.createElement(React.Fragment, null,
1761
+ React.createElement("input", { type: "file", "data-testid": "upload-file-input", style: { display: 'none' }, ref: fileInputRef, onChange: (e) => onFileChange(e) }),
1762
+ props.children({ onClick })));
1763
+ }
1764
+
1765
+ function AttachmentArrayInput(props) {
1766
+ const [values, setValues] = React.useState(props.defaultValue ?? []);
1767
+ const valuesRef = React.useRef();
1768
+ valuesRef.current = values;
1769
+ function setValuesWrapper(newValues) {
1770
+ setValues(newValues);
1771
+ if (props.onChange) {
1772
+ props.onChange(newValues);
1773
+ }
1774
+ }
1775
+ return (React.createElement("table", { style: { width: '100%' } },
1776
+ React.createElement("colgroup", null,
1777
+ React.createElement("col", { width: "97%" }),
1778
+ React.createElement("col", { width: "3%" })),
1779
+ React.createElement("tbody", null,
1780
+ values.map((v, index) => (React.createElement("tr", { key: `${index}-${values.length}` },
1781
+ React.createElement("td", null,
1782
+ React.createElement(AttachmentDisplay, { value: v, maxWidth: 200 })),
1783
+ React.createElement("td", null,
1784
+ React.createElement(core$1.ActionIcon, { title: "Remove", size: "sm", onClick: (e) => {
1785
+ killEvent(e);
1786
+ const copy = values.slice();
1787
+ copy.splice(index, 1);
1788
+ setValuesWrapper(copy);
1789
+ } },
1790
+ React.createElement(IconCircleMinus, null)))))),
1791
+ React.createElement("tr", null,
1792
+ React.createElement("td", null),
1793
+ React.createElement("td", null,
1794
+ React.createElement(AttachmentButton, { onUpload: (attachment) => {
1795
+ setValuesWrapper([...valuesRef.current, attachment]);
1796
+ } }, (props) => (React.createElement(core$1.ActionIcon, { ...props, title: "Add", size: "sm", color: "green" },
1797
+ React.createElement(IconCloudUpload, { size: 16 })))))))));
1798
+ }
1799
+
1800
+ function AttachmentInput(props) {
1801
+ const [value, setValue] = React.useState(props.defaultValue);
1802
+ function setValueWrapper(newValue) {
1803
+ setValue(newValue);
1804
+ if (props.onChange) {
1805
+ props.onChange(newValue);
1806
+ }
1807
+ }
1808
+ if (value) {
1809
+ return (React.createElement(React.Fragment, null,
1810
+ React.createElement(AttachmentDisplay, { value: value, maxWidth: 200 }),
1811
+ React.createElement(core$1.Button, { onClick: (e) => {
1812
+ killEvent(e);
1813
+ setValueWrapper(undefined);
1814
+ } }, "Remove")));
1815
+ }
1816
+ return (React.createElement(AttachmentButton, { onUpload: setValueWrapper }, (props) => React.createElement(core$1.Button, { ...props }, "Upload...")));
1817
+ }
1818
+
1819
+ const DEFAULT_IGNORED_PROPERTIES = [
1820
+ 'meta',
1821
+ 'implicitRules',
1822
+ 'language',
1823
+ 'text',
1824
+ 'contained',
1825
+ 'extension',
1826
+ 'modifierExtension',
1827
+ ];
1828
+
1829
+ const useStyles$e = core$1.createStyles((theme) => ({
1830
+ root: {
1831
+ display: 'grid',
1832
+ gridTemplateColumns: '30% 70%',
1833
+ margin: 0,
1834
+ '& > dt, & > dd': {
1835
+ padding: `${theme.spacing.sm} ${theme.spacing.sm}`,
1836
+ borderTop: `0.1px solid ${theme.colors.gray[3]}`,
1837
+ margin: 0,
1838
+ },
1839
+ },
1840
+ compact: {
1841
+ gridTemplateColumns: '20% 80%',
1842
+ '& > dt, & > dd': {
1843
+ padding: `0 ${theme.spacing.xs} ${theme.spacing.xs} 0`,
1844
+ border: 0,
1845
+ },
1846
+ },
1847
+ }));
1848
+ function DescriptionList(props) {
1849
+ const { children, compact } = props;
1850
+ const { classes, cx } = useStyles$e();
1851
+ return React.createElement("dl", { className: cx(classes.root, { [classes.compact]: compact }) }, children);
1852
+ }
1853
+ function DescriptionListEntry(props) {
1854
+ return (React.createElement(React.Fragment, null,
1855
+ React.createElement("dt", null, props.term),
1856
+ React.createElement("dd", null, props.children)));
1857
+ }
1858
+
1859
+ function CodeableConceptDisplay(props) {
1860
+ return React.createElement(React.Fragment, null, core.formatCodeableConcept(props.value));
1861
+ }
1862
+
1662
1863
  function CodingDisplay(props) {
1663
1864
  return React.createElement(React.Fragment, null, core.formatCoding(props.value));
1664
1865
  }
@@ -1699,14 +1900,6 @@
1699
1900
  contactDetail.telecom?.map((telecom, index) => (React.createElement(ContactPointDisplay, { key: 'telecom-' + index, value: telecom })))));
1700
1901
  }
1701
1902
 
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
1903
  function IdentifierDisplay(props) {
1711
1904
  return (React.createElement("div", null,
1712
1905
  props.value?.system,
@@ -1737,50 +1930,6 @@
1737
1930
  React.createElement(QuantityDisplay, { value: value.denominator })));
1738
1931
  }
1739
1932
 
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
1933
  function ReferenceDisplay(props) {
1785
1934
  if (!props.value) {
1786
1935
  return null;
@@ -1947,69 +2096,36 @@
1947
2096
  React.createElement(core$1.Input.Wrapper, { id: props.htmlFor, label: props.title, description: props.description, withAsterisk: props.withAsterisk }, (() => null)()))));
1948
2097
  }
1949
2098
 
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));
2099
+ function getErrorsForInput(outcome, expression) {
2100
+ return outcome?.issue
2101
+ ?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression))
2102
+ ?.map((issue) => issue.details?.text)
2103
+ ?.join('\n');
1952
2104
  }
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;
2105
+ function getIssuesForExpression(outcome, expression) {
2106
+ return outcome?.issue?.filter((issue) => isExpressionMatch(issue.expression?.[0], expression));
1993
2107
  }
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
- }
2108
+ function isExpressionMatch(expr1, expr2) {
2109
+ // Expression can be either "fieldName" or "resourceType.fieldName"
2110
+ if (expr1 === expr2) {
2111
+ return true;
2011
2112
  }
2012
- return undefined;
2113
+ if (!expr1 || !expr2) {
2114
+ return false;
2115
+ }
2116
+ const dot1 = expr1.indexOf('.');
2117
+ if (dot1 >= 0 && expr1.substring(dot1 + 1) === expr2) {
2118
+ return true;
2119
+ }
2120
+ const dot2 = expr2.indexOf('.');
2121
+ if (dot2 >= 0 && expr2.substring(dot2 + 1) === expr1) {
2122
+ return true;
2123
+ }
2124
+ return false;
2125
+ }
2126
+
2127
+ function FormSection(props) {
2128
+ 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
2129
  }
2014
2130
 
2015
2131
  function ResourceForm(props) {
@@ -2058,46 +2174,6 @@
2058
2174
  return obj;
2059
2175
  }
2060
2176
 
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
2177
  function CodeableConceptInput(props) {
2102
2178
  const [value, setValue] = React.useState(props.defaultValue);
2103
2179
  function handleChange(newValues) {
@@ -2129,25 +2205,6 @@
2129
2205
  };
2130
2206
  }
2131
2207
 
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
2208
  function CodingInput(props) {
2152
2209
  const [value, setValue] = React.useState(props.defaultValue);
2153
2210
  function handleChange(newValues) {
@@ -2518,21 +2575,6 @@
2518
2575
  }) })));
2519
2576
  }
2520
2577
 
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
2578
  /**
2537
2579
  * Defines which search parameters will be used by the type ahead to search for each resourceType
2538
2580
  */
@@ -2924,7 +2966,7 @@
2924
2966
  })));
2925
2967
  }
2926
2968
 
2927
- const useStyles$b = core$1.createStyles((theme) => ({
2969
+ const useStyles$d = core$1.createStyles((theme) => ({
2928
2970
  table: {
2929
2971
  width: 350,
2930
2972
  '& th': {
@@ -2969,7 +3011,7 @@
2969
3011
  return date.toLocaleString('default', { month: 'long' }) + ' ' + date.getFullYear();
2970
3012
  }
2971
3013
  function CalendarInput(props) {
2972
- const { classes } = useStyles$b();
3014
+ const { classes } = useStyles$d();
2973
3015
  const { onChangeMonth, onClick } = props;
2974
3016
  const [month, setMonth] = React.useState(getStartMonth);
2975
3017
  function moveMonth(delta) {
@@ -3052,13 +3094,27 @@
3052
3094
  return false;
3053
3095
  }
3054
3096
 
3055
- const useStyles$a = core$1.createStyles((theme) => ({
3097
+ const useStyles$c = core$1.createStyles(() => ({
3098
+ root: {
3099
+ '@media (max-width: 800px)': {
3100
+ paddingLeft: 4,
3101
+ paddingRight: 4,
3102
+ },
3103
+ },
3104
+ }));
3105
+ function Container(props) {
3106
+ const { children, ...others } = props;
3107
+ const { classes } = useStyles$c();
3108
+ return (React.createElement(core$1.Container, { className: classes.root, ...others }, children));
3109
+ }
3110
+
3111
+ const useStyles$b = core$1.createStyles((theme) => ({
3056
3112
  noteBody: { fontSize: theme.fontSizes.sm },
3057
3113
  noteCite: { fontSize: theme.fontSizes.xs, marginBlockStart: 3 },
3058
3114
  noteRoot: { padding: 5 },
3059
3115
  }));
3060
3116
  function NoteDisplay({ value }) {
3061
- const { classes } = useStyles$a();
3117
+ const { classes } = useStyles$b();
3062
3118
  if (!value) {
3063
3119
  return null;
3064
3120
  }
@@ -3151,7 +3207,7 @@
3151
3207
  return React.createElement(core$1.Badge, { color: statusToColor[props.status] }, props.status);
3152
3208
  }
3153
3209
 
3154
- const useStyles$9 = core$1.createStyles((theme) => ({
3210
+ const useStyles$a = core$1.createStyles((theme) => ({
3155
3211
  table: {
3156
3212
  border: `0.1px solid ${theme.colors.gray[5]}`,
3157
3213
  borderCollapse: 'collapse',
@@ -3246,7 +3302,7 @@
3246
3302
  core.formatDateTime(specimen.receivedTime)))))))));
3247
3303
  }
3248
3304
  function ObservationTable(props) {
3249
- const { classes } = useStyles$9();
3305
+ const { classes } = useStyles$a();
3250
3306
  return (React.createElement("table", { className: classes.table },
3251
3307
  React.createElement("thead", null,
3252
3308
  React.createElement("tr", null,
@@ -3260,7 +3316,7 @@
3260
3316
  React.createElement("tbody", null, props.value?.map((observation, index) => (React.createElement(ObservationRow, { key: `obs-${observation.id}-${index}`, hideObservationNotes: props.hideObservationNotes, value: observation }))))));
3261
3317
  }
3262
3318
  function ObservationRow(props) {
3263
- const { classes, cx } = useStyles$9();
3319
+ const { classes, cx } = useStyles$a();
3264
3320
  const observation = useResource(props.value);
3265
3321
  if (!observation) {
3266
3322
  return null;
@@ -3310,6 +3366,35 @@
3310
3366
  return code === 'AA' || code === 'LL' || code === 'HH' || code === 'A';
3311
3367
  }
3312
3368
 
3369
+ const useStyles$9 = core$1.createStyles((theme, { width, fill }) => ({
3370
+ paper: {
3371
+ maxWidth: width,
3372
+ margin: `${theme.spacing.xl} auto`,
3373
+ padding: fill ? 0 : theme.spacing.md,
3374
+ '@media (max-width: 800px)': {
3375
+ padding: fill ? 0 : 8,
3376
+ },
3377
+ '& img': {
3378
+ width: '100%',
3379
+ maxWidth: '100%',
3380
+ },
3381
+ '& video': {
3382
+ width: '100%',
3383
+ maxWidth: '100%',
3384
+ },
3385
+ },
3386
+ }));
3387
+ const defaultProps$1 = {
3388
+ shadow: 'xs',
3389
+ radius: 'md',
3390
+ withBorder: true,
3391
+ };
3392
+ function Panel(props) {
3393
+ const { className, children, width, fill, unstyled, ...others } = core$1.useComponentDefaultProps('Panel', defaultProps$1, props);
3394
+ const { classes, cx } = useStyles$9({ width, fill }, { name: 'Panel', unstyled });
3395
+ return (React.createElement(core$1.Paper, { className: cx(classes.paper, className), ...others }, children));
3396
+ }
3397
+
3313
3398
  const useStyles$8 = core$1.createStyles((theme) => ({
3314
3399
  root: {
3315
3400
  borderCollapse: 'collapse',
@@ -3400,29 +3485,6 @@
3400
3485
  }, ignoreMissingValues: props.ignoreMissingValues }));
3401
3486
  }
3402
3487
 
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
3488
  function Timeline(props) {
3427
3489
  return React.createElement(Container, null, props.children);
3428
3490
  }
@@ -3783,10 +3845,21 @@
3783
3845
 
3784
3846
  function DefaultResourceTimeline(props) {
3785
3847
  return (React.createElement(ResourceTimeline, { value: props.resource, loadTimelineResources: async (medplum, resourceType, id) => {
3786
- return Promise.allSettled([medplum.readHistory(resourceType, id)]);
3848
+ const ref = `${resourceType}/${id}`;
3849
+ const _count = 100;
3850
+ return Promise.allSettled([
3851
+ medplum.readHistory(resourceType, id),
3852
+ medplum.search('Task', { _filter: `based-on eq ${ref} or focus eq ${ref} or subject eq ${ref}`, _count }),
3853
+ ]);
3787
3854
  } }));
3788
3855
  }
3789
3856
 
3857
+ function Document(props) {
3858
+ const { children, ...others } = props;
3859
+ return (React.createElement(Container, null,
3860
+ React.createElement(Panel, { ...others }, children)));
3861
+ }
3862
+
3790
3863
  function EncounterTimeline(props) {
3791
3864
  return (React.createElement(ResourceTimeline, { value: props.encounter, loadTimelineResources: async (medplum, _resourceType, id) => {
3792
3865
  return Promise.allSettled([
@@ -3895,6 +3968,7 @@
3895
3968
  'of-type': 'of type',
3896
3969
  missing: 'missing',
3897
3970
  identifier: 'identifier',
3971
+ iterate: 'iterate',
3898
3972
  };
3899
3973
  /**
3900
3974
  * Sets the array of filters.
@@ -4565,7 +4639,7 @@
4565
4639
  return null;
4566
4640
  }
4567
4641
  const resourceType = props.search.resourceType;
4568
- const searchParams = core.globalSchema.types[resourceType].searchParams;
4642
+ const searchParams = core.globalSchema.types[resourceType].searchParams ?? {};
4569
4643
  const filters = search.filters || [];
4570
4644
  return (React.createElement(core$1.Modal, { title: "Filters", closeButtonProps: { 'aria-label': 'Close' }, size: 900, opened: props.visible, onClose: props.onCancel },
4571
4645
  React.createElement("div", null,
@@ -5056,8 +5130,8 @@
5056
5130
  return (React.createElement("div", { className: classes.root, "data-testid": "search-control" },
5057
5131
  !props.hideToolbar && (React.createElement(core$1.Group, { position: "apart", mb: "xl" },
5058
5132
  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"),
5133
+ React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconColumns, { size: iconSize }), onClick: () => setState({ ...stateRef.current, fieldEditorVisible: true }) }, "Fields"),
5134
+ 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
5135
  props.onNew && (React.createElement(core$1.Button, { compact: true, variant: buttonVariant, color: buttonColor, leftIcon: React.createElement(IconFilePlus, { size: iconSize }), onClick: props.onNew }, "New...")),
5062
5136
  !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
5137
  !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,16 +5367,37 @@
5293
5367
  }
5294
5368
  const MemoizedFhirPathTable = React.memo(FhirPathTable);
5295
5369
 
5370
+ function Logo(props) {
5371
+ return (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 491 491", style: { width: props.size, height: props.size } },
5372
+ React.createElement("title", null, "Medplum Logo"),
5373
+ 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" }),
5374
+ 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" }),
5375
+ 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" }),
5376
+ 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" }),
5377
+ React.createElement("path", { fill: props.fill || '#33961e', d: "M252 48A421 421 0 0057 270l-27-7A176 176 0 01251 43l1 5z" })));
5378
+ }
5379
+
5380
+ function OperationOutcomeAlert(props) {
5381
+ const issues = props.outcome?.issue || props.issues;
5382
+ if (!issues) {
5383
+ return null;
5384
+ }
5385
+ 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)))));
5386
+ }
5387
+
5296
5388
  function PatientTimeline(props) {
5297
- const loadTimelineResources = React.useCallback((medplum, _resourceType, id) => {
5389
+ const loadTimelineResources = React.useCallback((medplum, resourceType, id) => {
5390
+ const ref = `${resourceType}/${id}`;
5391
+ const _count = 100;
5298
5392
  return Promise.allSettled([
5299
5393
  medplum.readHistory('Patient', id),
5300
- medplum.search('Communication', 'subject=Patient/' + id),
5301
- medplum.search('Device', 'patient=Patient/' + id),
5302
- medplum.search('DeviceRequest', 'patient=Patient/' + id),
5303
- medplum.search('DiagnosticReport', 'subject=Patient/' + id),
5304
- medplum.search('Media', 'subject=Patient/' + id),
5305
- medplum.search('ServiceRequest', 'subject=Patient/' + id),
5394
+ medplum.search('Communication', { subject: ref, _count }),
5395
+ medplum.search('Device', { patient: ref, _count }),
5396
+ medplum.search('DeviceRequest', { patient: ref, _count }),
5397
+ medplum.search('DiagnosticReport', { subject: ref, _count }),
5398
+ medplum.search('Media', { subject: ref, _count }),
5399
+ medplum.search('ServiceRequest', { subject: ref, _count }),
5400
+ medplum.search('Task', { subject: ref, _count }),
5306
5401
  ]);
5307
5402
  }, []);
5308
5403
  return (React.createElement(ResourceTimeline, { value: props.patient, loadTimelineResources: loadTimelineResources, createCommunication: (resource, sender, text) => ({
@@ -6892,12 +6987,15 @@
6892
6987
  }
6893
6988
 
6894
6989
  function ServiceRequestTimeline(props) {
6895
- return (React.createElement(ResourceTimeline, { value: props.serviceRequest, loadTimelineResources: async (medplum, _resourceType, id) => {
6990
+ return (React.createElement(ResourceTimeline, { value: props.serviceRequest, loadTimelineResources: async (medplum, resourceType, id) => {
6991
+ const ref = `${resourceType}/${id}`;
6992
+ const _count = 100;
6896
6993
  return Promise.allSettled([
6897
6994
  medplum.readHistory('ServiceRequest', id),
6898
- medplum.search('Communication', 'based-on=ServiceRequest/' + id),
6899
- medplum.search('Media', '_count=100&based-on=ServiceRequest/' + id),
6900
- medplum.search('DiagnosticReport', 'based-on=ServiceRequest/' + id),
6995
+ medplum.search('Communication', { 'based-on': ref, _count }),
6996
+ medplum.search('DiagnosticReport', { 'based-on': ref, _count }),
6997
+ medplum.search('Media', { 'based-on': ref, _count }),
6998
+ medplum.search('Task', { _filter: `based-on eq ${ref} or focus eq ${ref} or subject eq ${ref}`, _count }),
6901
6999
  ]);
6902
7000
  }, createCommunication: (resource, sender, text) => ({
6903
7001
  resourceType: 'Communication',
@@ -6918,9 +7016,455 @@
6918
7016
  }) }));
6919
7017
  }
6920
7018
 
7019
+ function NewProjectForm(props) {
7020
+ const medplum = useMedplum();
7021
+ const [outcome, setOutcome] = React.useState();
7022
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: async (formData) => {
7023
+ try {
7024
+ props.handleAuthResponse(await medplum.startNewProject({
7025
+ login: props.login,
7026
+ projectName: formData.projectName,
7027
+ }));
7028
+ }
7029
+ catch (err) {
7030
+ setOutcome(err);
7031
+ }
7032
+ } },
7033
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7034
+ React.createElement(Logo, { size: 32 }),
7035
+ React.createElement(core$1.Title, null, "Create project")),
7036
+ React.createElement(core$1.Stack, { spacing: "xl" },
7037
+ React.createElement(core$1.TextInput, { name: "projectName", label: "Project Name", placeholder: "My Project", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'firstName') }),
7038
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
7039
+ "By clicking submit you agree to the Medplum",
7040
+ ' ',
7041
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/privacy" }, "Privacy\u00A0Policy"),
7042
+ ' and ',
7043
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/terms" }, "Terms\u00A0of\u00A0Service"),
7044
+ ".")),
7045
+ React.createElement(core$1.Group, { position: "right", mt: "xl", noWrap: true },
7046
+ React.createElement(core$1.Button, { type: "submit" }, "Create project"))));
7047
+ }
7048
+
7049
+ /**
7050
+ * Dynamically creates a script tag for the specified JavaScript file.
7051
+ * @param src The JavaScript file URL.
7052
+ */
7053
+ function createScriptTag(src, onload) {
7054
+ const head = document.getElementsByTagName('head')[0];
7055
+ const script = document.createElement('script');
7056
+ script.async = true;
7057
+ script.src = src;
7058
+ script.onload = onload || null;
7059
+ head.appendChild(script);
7060
+ }
7061
+
7062
+ function GoogleButton(props) {
7063
+ const medplum = useMedplum();
7064
+ const { googleClientId, handleGoogleCredential } = props;
7065
+ const parentRef = React.useRef(null);
7066
+ const [scriptLoaded, setScriptLoaded] = React.useState(typeof google !== 'undefined');
7067
+ const [initialized, setInitialized] = React.useState(false);
7068
+ const [buttonRendered, setButtonRendered] = React.useState(false);
7069
+ React.useEffect(() => {
7070
+ if (typeof google === 'undefined') {
7071
+ createScriptTag('https://accounts.google.com/gsi/client', () => setScriptLoaded(true));
7072
+ return;
7073
+ }
7074
+ if (!initialized) {
7075
+ google.accounts.id.initialize({
7076
+ client_id: googleClientId,
7077
+ callback: handleGoogleCredential,
7078
+ });
7079
+ setInitialized(true);
7080
+ }
7081
+ if (parentRef.current && !buttonRendered) {
7082
+ google.accounts.id.renderButton(parentRef.current, {});
7083
+ setButtonRendered(true);
7084
+ }
7085
+ }, [medplum, googleClientId, initialized, scriptLoaded, parentRef, buttonRendered, handleGoogleCredential]);
7086
+ if (!googleClientId) {
7087
+ return null;
7088
+ }
7089
+ return React.createElement("div", { ref: parentRef });
7090
+ }
7091
+ function getGoogleClientId(clientId) {
7092
+ if (clientId) {
7093
+ return clientId;
7094
+ }
7095
+ if (typeof window !== 'undefined') {
7096
+ const origin = window.location.protocol + '//' + window.location.host;
7097
+ const authorizedOrigins = "undefined"?.split(',') ?? [];
7098
+ if (authorizedOrigins.includes(origin)) {
7099
+ return "undefined";
7100
+ }
7101
+ }
7102
+ return undefined;
7103
+ }
7104
+
7105
+ /**
7106
+ * Dynamically loads the recaptcha script.
7107
+ * We do not want to load the script on page load unless the user needs it.
7108
+ * @param siteKey The reCAPTCHA site key, available from the reCAPTCHA admin page.
7109
+ */
7110
+ function initRecaptcha(siteKey) {
7111
+ if (typeof grecaptcha === 'undefined') {
7112
+ createScriptTag('https://www.google.com/recaptcha/api.js?render=' + siteKey);
7113
+ }
7114
+ }
7115
+ /**
7116
+ * Starts a request to generate a recapcha token.
7117
+ * @param siteKey The reCAPTCHA site key, available from the reCAPTCHA admin page.
7118
+ * @returns Promise to a recaptcha token for the current user.
7119
+ */
7120
+ function getRecaptcha(siteKey) {
7121
+ return new Promise((resolve, reject) => {
7122
+ grecaptcha.ready(async () => {
7123
+ try {
7124
+ resolve(await grecaptcha.execute(siteKey, { action: 'submit' }));
7125
+ }
7126
+ catch (err) {
7127
+ reject(err);
7128
+ }
7129
+ });
7130
+ });
7131
+ }
7132
+
7133
+ function NewUserForm(props) {
7134
+ const googleClientId = getGoogleClientId(props.googleClientId);
7135
+ const recaptchaSiteKey = props.recaptchaSiteKey;
7136
+ const medplum = useMedplum();
7137
+ const [outcome, setOutcome] = React.useState();
7138
+ const issues = getIssuesForExpression(outcome, undefined);
7139
+ React.useEffect(() => {
7140
+ if (recaptchaSiteKey) {
7141
+ initRecaptcha(recaptchaSiteKey);
7142
+ }
7143
+ }, [recaptchaSiteKey]);
7144
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: async (formData) => {
7145
+ try {
7146
+ let recaptchaToken = '';
7147
+ if (recaptchaSiteKey) {
7148
+ recaptchaToken = await getRecaptcha(recaptchaSiteKey);
7149
+ }
7150
+ props.handleAuthResponse(await medplum.startNewUser({
7151
+ projectId: props.projectId,
7152
+ firstName: formData.firstName,
7153
+ lastName: formData.lastName,
7154
+ email: formData.email,
7155
+ password: formData.password,
7156
+ remember: formData.remember === 'true',
7157
+ recaptchaSiteKey,
7158
+ recaptchaToken,
7159
+ }));
7160
+ }
7161
+ catch (err) {
7162
+ setOutcome(err);
7163
+ }
7164
+ } },
7165
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, props.children),
7166
+ React.createElement(OperationOutcomeAlert, { issues: issues }),
7167
+ googleClientId && (React.createElement(React.Fragment, null,
7168
+ React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
7169
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: async (response) => {
7170
+ try {
7171
+ props.handleAuthResponse(await medplum.startGoogleLogin({
7172
+ googleClientId: response.clientId,
7173
+ googleCredential: response.credential,
7174
+ createUser: true,
7175
+ }));
7176
+ }
7177
+ catch (err) {
7178
+ setOutcome(err);
7179
+ }
7180
+ } })),
7181
+ React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
7182
+ React.createElement(core$1.Stack, { spacing: "xl" },
7183
+ React.createElement(core$1.TextInput, { name: "firstName", type: "text", label: "First name", placeholder: "First name", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'firstName') }),
7184
+ React.createElement(core$1.TextInput, { name: "lastName", type: "text", label: "Last name", placeholder: "Last name", required: true, error: getErrorsForInput(outcome, 'lastName') }),
7185
+ React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, error: getErrorsForInput(outcome, 'email') }),
7186
+ React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, error: getErrorsForInput(outcome, 'password') }),
7187
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
7188
+ "By clicking submit you agree to the Medplum",
7189
+ ' ',
7190
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/privacy" }, "Privacy\u00A0Policy"),
7191
+ ' and ',
7192
+ React.createElement(core$1.Anchor, { href: "https://www.medplum.com/terms" }, "Terms\u00A0of\u00A0Service"),
7193
+ "."),
7194
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" },
7195
+ "This site is protected by reCAPTCHA and the Google",
7196
+ ' ',
7197
+ React.createElement(core$1.Anchor, { href: "https://policies.google.com/privacy" }, "Privacy\u00A0Policy"),
7198
+ ' and ',
7199
+ React.createElement(core$1.Anchor, { href: "https://policies.google.com/terms" }, "Terms\u00A0of\u00A0Service"),
7200
+ " apply.")),
7201
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", noWrap: true },
7202
+ React.createElement(core$1.Checkbox, { name: "remember", label: "Remember me", size: "xs" }),
7203
+ React.createElement(core$1.Button, { type: "submit" }, "Create account"))));
7204
+ }
7205
+
7206
+ function RegisterForm(props) {
7207
+ const { type, projectId, googleClientId, recaptchaSiteKey, onSuccess } = props;
7208
+ const medplum = useMedplum();
7209
+ const [login, setLogin] = React.useState(undefined);
7210
+ const [outcome, setOutcome] = React.useState();
7211
+ React.useEffect(() => {
7212
+ if (type === 'patient' && login) {
7213
+ medplum
7214
+ .startNewPatient({ login, projectId: projectId })
7215
+ .then((response) => medplum.processCode(response.code))
7216
+ .then(() => onSuccess())
7217
+ .catch((err) => setOutcome(err));
7218
+ }
7219
+ }, [medplum, type, projectId, login, onSuccess]);
7220
+ function handleAuthResponse(response) {
7221
+ if (response.code) {
7222
+ medplum
7223
+ .processCode(response.code)
7224
+ .then(() => onSuccess())
7225
+ .catch(console.log);
7226
+ }
7227
+ else if (response.login) {
7228
+ setLogin(response.login);
7229
+ }
7230
+ }
7231
+ return (React.createElement(Document, { width: 450 },
7232
+ outcome && React.createElement("pre", null, JSON.stringify(outcome, null, 2)),
7233
+ !login && (React.createElement(NewUserForm, { projectId: projectId, googleClientId: googleClientId, recaptchaSiteKey: recaptchaSiteKey, handleAuthResponse: handleAuthResponse }, props.children)),
7234
+ login && type === 'project' && React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse })));
7235
+ }
7236
+
7237
+ function AuthenticationForm(props) {
7238
+ const [email, setEmail] = React.useState();
7239
+ if (!email) {
7240
+ return React.createElement(EmailForm, { setEmail: setEmail, ...props });
7241
+ }
7242
+ else {
7243
+ return React.createElement(PasswordForm, { email: email, ...props });
7244
+ }
7245
+ }
7246
+ function EmailForm(props) {
7247
+ const { setEmail, onRegister, handleAuthResponse, children, ...baseLoginRequest } = props;
7248
+ const medplum = useMedplum();
7249
+ const googleClientId = !props.disableGoogleAuth && getGoogleClientId(props.googleClientId);
7250
+ const isExternalAuth = React.useCallback(async (authMethod) => {
7251
+ if (!authMethod.authorizeUrl) {
7252
+ return false;
7253
+ }
7254
+ const state = JSON.stringify({
7255
+ ...(await medplum.ensureCodeChallenge(baseLoginRequest)),
7256
+ domain: authMethod.domain,
7257
+ });
7258
+ const url = new URL(authMethod.authorizeUrl);
7259
+ url.searchParams.set('state', state);
7260
+ window.location.assign(url.toString());
7261
+ return true;
7262
+ }, [medplum, baseLoginRequest]);
7263
+ const handleSubmit = React.useCallback(async (formData) => {
7264
+ const authMethod = await medplum.post('auth/method', { email: formData.email });
7265
+ if (!(await isExternalAuth(authMethod))) {
7266
+ setEmail(formData.email);
7267
+ }
7268
+ }, [medplum, isExternalAuth, setEmail]);
7269
+ const handleGoogleCredential = React.useCallback(async (response) => {
7270
+ const authResponse = await medplum.startGoogleLogin({
7271
+ ...baseLoginRequest,
7272
+ googleCredential: response.credential,
7273
+ });
7274
+ if (!(await isExternalAuth(authResponse))) {
7275
+ handleAuthResponse(authResponse);
7276
+ }
7277
+ }, [medplum, baseLoginRequest, isExternalAuth, handleAuthResponse]);
7278
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
7279
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
7280
+ googleClientId && (React.createElement(React.Fragment, null,
7281
+ React.createElement(core$1.Group, { position: "center", p: "xl", style: { height: 70 } },
7282
+ React.createElement(GoogleButton, { googleClientId: googleClientId, handleGoogleCredential: handleGoogleCredential })),
7283
+ React.createElement(core$1.Divider, { label: "or", labelPosition: "center", my: "lg" }))),
7284
+ React.createElement(core$1.TextInput, { name: "email", type: "email", label: "Email", placeholder: "name@domain.com", required: true, autoFocus: true }),
7285
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
7286
+ React.createElement("div", null, onRegister && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onRegister, size: "xs" }, "Register"))),
7287
+ React.createElement(core$1.Button, { type: "submit" }, "Next"))));
7288
+ }
7289
+ function PasswordForm(props) {
7290
+ const { onForgotPassword, handleAuthResponse, children, ...baseLoginRequest } = props;
7291
+ const medplum = useMedplum();
7292
+ const [outcome, setOutcome] = React.useState();
7293
+ const issues = getIssuesForExpression(outcome, undefined);
7294
+ const handleSubmit = React.useCallback((formData) => {
7295
+ medplum
7296
+ .startLogin({
7297
+ ...baseLoginRequest,
7298
+ password: formData.password,
7299
+ remember: formData.remember === 'on',
7300
+ })
7301
+ .then(handleAuthResponse)
7302
+ .catch((err) => setOutcome(core.normalizeOperationOutcome(err)));
7303
+ }, [medplum, baseLoginRequest, handleAuthResponse]);
7304
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: handleSubmit },
7305
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
7306
+ React.createElement(OperationOutcomeAlert, { issues: issues }),
7307
+ React.createElement(core$1.Stack, { spacing: "xl" },
7308
+ React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'password') })),
7309
+ React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
7310
+ onForgotPassword && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onForgotPassword, size: "xs" }, "Forgot password")),
7311
+ React.createElement(core$1.Checkbox, { id: "remember", name: "remember", label: "Remember me", size: "xs", sx: { lineHeight: 1 } }),
7312
+ React.createElement(core$1.Button, { type: "submit" }, "Sign in"))));
7313
+ }
7314
+
7315
+ function ChooseProfileForm(props) {
7316
+ const medplum = useMedplum();
7317
+ const [outcome, setOutcome] = React.useState();
7318
+ return (React.createElement(core$1.Stack, null,
7319
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7320
+ React.createElement(Logo, { size: 32 }),
7321
+ React.createElement(core$1.Title, { order: 3 }, "Choose profile")),
7322
+ React.createElement(OperationOutcomeAlert, { outcome: outcome }),
7323
+ props.memberships.map((membership) => (React.createElement(core$1.UnstyledButton, { key: membership.id, onClick: () => {
7324
+ medplum
7325
+ .post('auth/profile', {
7326
+ login: props.login,
7327
+ profile: membership.id,
7328
+ })
7329
+ .then(props.handleAuthResponse)
7330
+ .catch((err) => setOutcome(core.normalizeOperationOutcome(err)));
7331
+ } },
7332
+ React.createElement(core$1.Group, null,
7333
+ React.createElement(core$1.Avatar, { radius: "xl" }),
7334
+ React.createElement("div", { style: { flex: 1 } },
7335
+ React.createElement(core$1.Text, { size: "sm", weight: 500 }, membership.profile?.display),
7336
+ React.createElement(core$1.Text, { color: "dimmed", size: "xs" }, membership.project?.display))))))));
7337
+ }
7338
+
7339
+ function ChooseScopeForm(props) {
7340
+ const medplum = useMedplum();
7341
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
7342
+ medplum
7343
+ .post('auth/scope', {
7344
+ login: props.login,
7345
+ scope: Object.keys(formData).join(' '),
7346
+ })
7347
+ .then(props.handleAuthResponse)
7348
+ .catch(console.log);
7349
+ } },
7350
+ React.createElement(core$1.Stack, null,
7351
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7352
+ React.createElement(Logo, { size: 32 }),
7353
+ React.createElement(core$1.Title, null, "Choose scope")),
7354
+ 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 })))),
7355
+ React.createElement(core$1.Group, { position: "right", mt: "xl" },
7356
+ React.createElement(core$1.Button, { type: "submit" }, "Set scope")))));
7357
+ }
7358
+
7359
+ function MfaForm(props) {
7360
+ const medplum = useMedplum();
7361
+ const [errorMessage, setErrorMessage] = React.useState(undefined);
7362
+ return (React.createElement(Form, { style: { maxWidth: 400 }, onSubmit: (formData) => {
7363
+ setErrorMessage(undefined);
7364
+ medplum
7365
+ .post('auth/mfa/verify', {
7366
+ login: props.login,
7367
+ token: formData.token,
7368
+ })
7369
+ .then(props.handleAuthResponse)
7370
+ .catch((err) => setErrorMessage(core.normalizeErrorString(err)));
7371
+ } },
7372
+ React.createElement(core$1.Stack, null,
7373
+ React.createElement(core$1.Center, { sx: { flexDirection: 'column' } },
7374
+ React.createElement(Logo, { size: 32 }),
7375
+ React.createElement(core$1.Title, null, "Enter MFA code")),
7376
+ errorMessage && (React.createElement(core$1.Alert, { icon: React.createElement(IconAlertCircle, { size: 16 }), title: "Error", color: "red" }, errorMessage)),
7377
+ React.createElement(core$1.Stack, null,
7378
+ React.createElement(core$1.TextInput, { name: "token", label: "MFA code", required: true })),
7379
+ React.createElement(core$1.Group, { position: "right", mt: "xl" },
7380
+ React.createElement(core$1.Button, { type: "submit" }, "Submit code")))));
7381
+ }
7382
+
7383
+ /**
7384
+ * The SignInForm component allows users to sign in to Medplum.
7385
+ *
7386
+ * "Signing in" is a multi-step process:
7387
+ * 1) Authentication - identify the user
7388
+ * 2) MFA - If MFA is enabled, prompt for MFA code
7389
+ * 3) Choose profile - If the user has multiple profiles, prompt to choose one
7390
+ * 4) Choose scope - If the user has multiple scopes, prompt to choose one
7391
+ * 5) Success - Return to the caller with either a code or a redirect
7392
+ */
7393
+ function SignInForm(props) {
7394
+ const { chooseScopes, onSuccess, onForgotPassword, onRegister, onCode, ...baseLoginRequest } = props;
7395
+ const medplum = useMedplum();
7396
+ const [login, setLogin] = React.useState(undefined);
7397
+ const [mfaRequired, setAuthenticatorRequired] = React.useState(false);
7398
+ const [memberships, setMemberships] = React.useState(undefined);
7399
+ const handleCode = React.useCallback((code) => {
7400
+ if (onCode) {
7401
+ onCode(code);
7402
+ }
7403
+ else {
7404
+ medplum
7405
+ .processCode(code)
7406
+ .then(() => {
7407
+ if (onSuccess) {
7408
+ onSuccess();
7409
+ }
7410
+ })
7411
+ .catch(console.log);
7412
+ }
7413
+ }, [medplum, onCode, onSuccess]);
7414
+ const handleAuthResponse = React.useCallback((response) => {
7415
+ setAuthenticatorRequired(!!response.mfaRequired);
7416
+ if (response.login) {
7417
+ setLogin(response.login);
7418
+ }
7419
+ if (response.memberships) {
7420
+ setMemberships(response.memberships);
7421
+ }
7422
+ if (response.code) {
7423
+ if (chooseScopes) {
7424
+ setMemberships(undefined);
7425
+ }
7426
+ else {
7427
+ handleCode(response.code);
7428
+ }
7429
+ }
7430
+ }, [chooseScopes, handleCode]);
7431
+ const handleScopeResponse = React.useCallback((response) => {
7432
+ handleCode(response.code);
7433
+ }, [handleCode]);
7434
+ React.useEffect(() => {
7435
+ if (props.login) {
7436
+ medplum
7437
+ .get('auth/login/' + props.login)
7438
+ .then(handleAuthResponse)
7439
+ .catch(console.error);
7440
+ }
7441
+ }, [medplum, props, handleAuthResponse]);
7442
+ return (React.createElement(Document, { width: 450 }, (() => {
7443
+ if (!login) {
7444
+ return (React.createElement(AuthenticationForm, { onForgotPassword: onForgotPassword, onRegister: onRegister, handleAuthResponse: handleAuthResponse, disableGoogleAuth: props.disableGoogleAuth, ...baseLoginRequest }, props.children));
7445
+ }
7446
+ else if (mfaRequired) {
7447
+ return React.createElement(MfaForm, { login: login, handleAuthResponse: handleAuthResponse });
7448
+ }
7449
+ else if (memberships) {
7450
+ return React.createElement(ChooseProfileForm, { login: login, memberships: memberships, handleAuthResponse: handleAuthResponse });
7451
+ }
7452
+ else if (props.projectId === 'new') {
7453
+ return React.createElement(NewProjectForm, { login: login, handleAuthResponse: handleAuthResponse });
7454
+ }
7455
+ else if (props.chooseScopes) {
7456
+ return React.createElement(ChooseScopeForm, { login: login, scope: props.scope, handleAuthResponse: handleScopeResponse });
7457
+ }
7458
+ else {
7459
+ return React.createElement("div", null, "Success");
7460
+ }
7461
+ })()));
7462
+ }
7463
+
6921
7464
  exports.AddressDisplay = AddressDisplay;
6922
7465
  exports.AddressInput = AddressInput;
6923
7466
  exports.AnnotationInput = AnnotationInput;
7467
+ exports.AppShell = AppShell;
6924
7468
  exports.AsyncAutocomplete = AsyncAutocomplete;
6925
7469
  exports.AttachmentArrayDisplay = AttachmentArrayDisplay;
6926
7470
  exports.AttachmentArrayInput = AttachmentArrayInput;
@@ -6954,10 +7498,12 @@
6954
7498
  exports.FhirPathTable = FhirPathTable;
6955
7499
  exports.Form = Form;
6956
7500
  exports.FormSection = FormSection;
7501
+ exports.Header = Header;
6957
7502
  exports.HumanNameDisplay = HumanNameDisplay;
6958
7503
  exports.HumanNameInput = HumanNameInput;
6959
7504
  exports.IdentifierDisplay = IdentifierDisplay;
6960
7505
  exports.IdentifierInput = IdentifierInput;
7506
+ exports.Loading = Loading;
6961
7507
  exports.Logo = Logo;
6962
7508
  exports.MedplumLink = MedplumLink;
6963
7509
  exports.MedplumProvider = MedplumProvider;
@@ -6965,6 +7511,7 @@
6965
7511
  exports.MemoizedSearchControl = MemoizedSearchControl;
6966
7512
  exports.MoneyDisplay = MoneyDisplay;
6967
7513
  exports.MoneyInput = MoneyInput;
7514
+ exports.Navbar = Navbar;
6968
7515
  exports.ObservationTable = ObservationTable;
6969
7516
  exports.OperationOutcomeAlert = OperationOutcomeAlert;
6970
7517
  exports.Panel = Panel;