@medplum/react 2.0.15 → 2.0.17

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