@medplum/react 2.0.17 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/cjs/index.cjs +149 -84
  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 +1 -1
  5. package/dist/esm/AppShell/AppShell.mjs.map +1 -1
  6. package/dist/esm/AppShell/Navbar.mjs +21 -14
  7. package/dist/esm/AppShell/Navbar.mjs.map +1 -1
  8. package/dist/esm/BookmarkDialog/BookmarkDialog.mjs +49 -0
  9. package/dist/esm/BookmarkDialog/BookmarkDialog.mjs.map +1 -0
  10. package/dist/esm/DefaultResourceTimeline/DefaultResourceTimeline.mjs +6 -1
  11. package/dist/esm/DefaultResourceTimeline/DefaultResourceTimeline.mjs.map +1 -1
  12. package/dist/esm/PatientTimeline/PatientTimeline.mjs +10 -7
  13. package/dist/esm/PatientTimeline/PatientTimeline.mjs.map +1 -1
  14. package/dist/esm/SearchControl/SearchUtils.mjs +1 -0
  15. package/dist/esm/SearchControl/SearchUtils.mjs.map +1 -1
  16. package/dist/esm/SearchFilterEditor/SearchFilterEditor.mjs +1 -1
  17. package/dist/esm/SearchFilterEditor/SearchFilterEditor.mjs.map +1 -1
  18. package/dist/esm/ServiceRequestTimeline/ServiceRequestTimeline.mjs +7 -4
  19. package/dist/esm/ServiceRequestTimeline/ServiceRequestTimeline.mjs.map +1 -1
  20. package/dist/esm/auth/AuthenticationForm.mjs +1 -1
  21. package/dist/esm/auth/AuthenticationForm.mjs.map +1 -1
  22. package/dist/esm/index.min.mjs +1 -1
  23. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPlus.mjs +13 -0
  24. package/dist/esm/node_modules/@tabler/icons-react/dist/esm/icons/IconPlus.mjs.map +1 -0
  25. package/dist/types/AppShell/AppShell.d.ts +1 -0
  26. package/dist/types/AppShell/Navbar.d.ts +1 -0
  27. package/dist/types/BookmarkDialog/BookmarkDialog.d.ts +8 -0
  28. package/package.json +1 -1
@@ -680,6 +680,15 @@
680
680
  ["path", { d: "M14.5 4l5.5 5.5", key: "svg-3" }]
681
681
  ]);
682
682
 
683
+ /**
684
+ * @tabler/icons-react v2.17.0 - MIT
685
+ */
686
+
687
+ var IconPlus = createReactComponent("plus", "IconPlus", [
688
+ ["path", { d: "M12 5l0 14", key: "svg-0" }],
689
+ ["path", { d: "M5 12l14 0", key: "svg-1" }]
690
+ ]);
691
+
683
692
  /**
684
693
  * @tabler/icons-react v2.17.0 - MIT
685
694
  */
@@ -1390,6 +1399,102 @@
1390
1399
  React.createElement(core$1.Text, { size: "xs", color: "dimmed", align: "center" }, props.version))))));
1391
1400
  }
1392
1401
 
1402
+ /**
1403
+ * Parses an HTML form and returns the result as a JavaScript object.
1404
+ * @param form The HTML form element.
1405
+ */
1406
+ function parseForm(form) {
1407
+ const result = {};
1408
+ for (const element of Array.from(form.elements)) {
1409
+ if (element instanceof HTMLInputElement) {
1410
+ parseInputElement(result, element);
1411
+ }
1412
+ else if (element instanceof HTMLTextAreaElement) {
1413
+ result[element.name] = element.value;
1414
+ }
1415
+ else if (element instanceof HTMLSelectElement) {
1416
+ parseSelectElement(result, element);
1417
+ }
1418
+ }
1419
+ return result;
1420
+ }
1421
+ /**
1422
+ * Parses an HTML input element.
1423
+ * Sets the name/value pair in the result,
1424
+ * but only if the element is enabled and checked.
1425
+ * @param el The input element.
1426
+ * @param result The result builder.
1427
+ */
1428
+ function parseInputElement(result, el) {
1429
+ if (el.disabled) {
1430
+ // Ignore disabled elements
1431
+ return;
1432
+ }
1433
+ if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked) {
1434
+ // Ignore unchecked radio or checkbox elements
1435
+ return;
1436
+ }
1437
+ result[el.name] = el.value;
1438
+ }
1439
+ /**
1440
+ * Parses an HTML select element.
1441
+ * Sets the name/value pair if one is selected.
1442
+ * @param result The result builder.
1443
+ * @param el The select element.
1444
+ */
1445
+ function parseSelectElement(result, el) {
1446
+ result[el.name] = el.value;
1447
+ }
1448
+
1449
+ function Form(props) {
1450
+ return (React.createElement("form", { style: props.style, "data-testid": props.testid, onSubmit: (e) => {
1451
+ e.preventDefault();
1452
+ const formData = parseForm(e.target);
1453
+ if (props.onSubmit) {
1454
+ props.onSubmit(formData);
1455
+ }
1456
+ } }, props.children));
1457
+ }
1458
+
1459
+ function BookmarkDialog(props) {
1460
+ const medplum = useMedplum();
1461
+ const config = medplum.getUserConfiguration();
1462
+ const location = reactRouterDom.useLocation();
1463
+ function submitHandler(formData) {
1464
+ const { menuname, bookmarkname: name } = formData;
1465
+ const target = location.pathname + location.search;
1466
+ const newConfig = core.deepClone(config);
1467
+ const menu = newConfig?.menu?.find(({ title }) => title === menuname);
1468
+ menu?.link?.push({ name, target });
1469
+ medplum
1470
+ .updateResource(newConfig)
1471
+ .then((res) => {
1472
+ // refresh current config menu
1473
+ config.menu = res.menu;
1474
+ medplum.dispatchEvent({ type: 'change' });
1475
+ notifications.showNotification({ color: 'green', message: 'Success' });
1476
+ props.onOk();
1477
+ })
1478
+ .catch((err) => {
1479
+ notifications.showNotification({ color: 'red', message: core.normalizeErrorString(err) });
1480
+ });
1481
+ }
1482
+ return (React.createElement(core$1.Modal, { title: "Add Bookmark", closeButtonProps: { 'aria-label': 'Close' }, opened: props.visible, onClose: props.onCancel },
1483
+ React.createElement(Form, { onSubmit: submitHandler },
1484
+ React.createElement(core$1.Stack, null,
1485
+ React.createElement(SelectMenu, { config: config }),
1486
+ React.createElement(core$1.TextInput, { label: "Bookmark Name", type: "text", name: "bookmarkname", placeholder: "bookmark name", withAsterisk: true }),
1487
+ React.createElement(core$1.Group, { position: "right" },
1488
+ React.createElement(core$1.Button, { mt: "sm", type: "submit" }, "OK"))))));
1489
+ }
1490
+ function SelectMenu(props) {
1491
+ function userConfigToMenu(config) {
1492
+ return config?.menu?.map((menu) => menu.title);
1493
+ }
1494
+ const menus = userConfigToMenu(props.config);
1495
+ return (React.createElement(core$1.NativeSelect, { name: "menuname", defaultValue: menus?.[0], label: "Select Menu Option", placeholder: "Menu", data: menus, withAsterisk: true }));
1496
+ }
1497
+
1393
1498
  function toKey(element) {
1394
1499
  return element.code;
1395
1500
  }
@@ -1498,6 +1603,7 @@
1498
1603
  function Navbar(props) {
1499
1604
  const { classes } = useStyles$f();
1500
1605
  const navigate = useMedplumNavigate();
1606
+ const [bookmarkDialogVisible, setBookmarkDialogVisible] = React.useState(false);
1501
1607
  function onLinkClick(e, to) {
1502
1608
  e.stopPropagation();
1503
1609
  e.preventDefault();
@@ -1511,18 +1617,22 @@
1511
1617
  navigate(`/${resourceType}`);
1512
1618
  }
1513
1619
  }
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)))))))))));
1620
+ return (React.createElement(React.Fragment, null,
1621
+ React.createElement(core$1.Navbar, { width: { sm: 250 }, p: "xs" },
1622
+ React.createElement(core$1.Navbar.Section, { mb: "sm" },
1623
+ React.createElement(CodeInput, { key: window.location.pathname, name: "resourceType", placeholder: "Resource Type", property: {
1624
+ binding: {
1625
+ valueSet: 'http://hl7.org/fhir/ValueSet/resource-types',
1626
+ },
1627
+ }, onChange: (newValue) => navigateResourceType(newValue), creatable: false, maxSelectedValues: 0, clearSearchOnChange: true, clearable: false })),
1628
+ React.createElement(core$1.Navbar.Section, { grow: true },
1629
+ props.menus?.map((menu) => (React.createElement(React.Fragment, { key: `menu-${menu.title}` },
1630
+ React.createElement(core$1.Text, { className: classes.menuTitle }, menu.title),
1631
+ menu.links?.map((link) => (React.createElement(NavbarLink, { key: link.href, to: link.href, onClick: (e) => onLinkClick(e, link.href) },
1632
+ React.createElement(NavLinkIcon, { to: link.href, icon: link.icon }),
1633
+ React.createElement("span", null, link.label))))))),
1634
+ props.displayAddBookmark && (React.createElement(core$1.Button, { variant: "subtle", size: "xs", mt: "xl", leftIcon: React.createElement(IconPlus, { size: "0.75rem" }), onClick: () => setBookmarkDialogVisible(true) }, "Add Bookmark")))),
1635
+ React.createElement(BookmarkDialog, { visible: bookmarkDialogVisible, onOk: () => setBookmarkDialogVisible(false), onCancel: () => setBookmarkDialogVisible(false) })));
1526
1636
  }
1527
1637
  function NavbarLink(props) {
1528
1638
  const { classes, cx } = useStyles$f();
@@ -1575,7 +1685,7 @@
1575
1685
  main: {
1576
1686
  background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0],
1577
1687
  },
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 },
1688
+ }, padding: 0, fixed: true, header: profile && React.createElement(Header, { logo: props.logo, version: props.version, navbarToggle: toggleNavbar }), navbar: profile && navbarOpen ? (React.createElement(Navbar, { menus: props.menus, closeNavbar: closeNavbar, displayAddBookmark: props.displayAddBookmark })) : undefined },
1579
1689
  React.createElement(ErrorBoundary, null,
1580
1690
  React.createElement(React.Suspense, { fallback: React.createElement(Loading, null) }, props.children))));
1581
1691
  }
@@ -3256,63 +3366,6 @@
3256
3366
  return code === 'AA' || code === 'LL' || code === 'HH' || code === 'A';
3257
3367
  }
3258
3368
 
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
3369
  const useStyles$9 = core$1.createStyles((theme, { width, fill }) => ({
3317
3370
  paper: {
3318
3371
  maxWidth: width,
@@ -3792,7 +3845,12 @@
3792
3845
 
3793
3846
  function DefaultResourceTimeline(props) {
3794
3847
  return (React.createElement(ResourceTimeline, { value: props.resource, loadTimelineResources: async (medplum, resourceType, id) => {
3795
- return Promise.allSettled([medplum.readHistory(resourceType, id)]);
3848
+ const ref = `${resourceType}/${id}`;
3849
+ const _count = 100;
3850
+ return Promise.allSettled([
3851
+ medplum.readHistory(resourceType, id),
3852
+ medplum.search('Task', { _filter: `based-on eq ${ref} or focus eq ${ref} or subject eq ${ref}`, _count }),
3853
+ ]);
3796
3854
  } }));
3797
3855
  }
3798
3856
 
@@ -3910,6 +3968,7 @@
3910
3968
  'of-type': 'of type',
3911
3969
  missing: 'missing',
3912
3970
  identifier: 'identifier',
3971
+ iterate: 'iterate',
3913
3972
  };
3914
3973
  /**
3915
3974
  * Sets the array of filters.
@@ -4580,7 +4639,7 @@
4580
4639
  return null;
4581
4640
  }
4582
4641
  const resourceType = props.search.resourceType;
4583
- const searchParams = core.globalSchema.types[resourceType].searchParams;
4642
+ const searchParams = core.globalSchema.types[resourceType].searchParams ?? {};
4584
4643
  const filters = search.filters || [];
4585
4644
  return (React.createElement(core$1.Modal, { title: "Filters", closeButtonProps: { 'aria-label': 'Close' }, size: 900, opened: props.visible, onClose: props.onCancel },
4586
4645
  React.createElement("div", null,
@@ -5327,15 +5386,18 @@
5327
5386
  }
5328
5387
 
5329
5388
  function PatientTimeline(props) {
5330
- const loadTimelineResources = React.useCallback((medplum, _resourceType, id) => {
5389
+ const loadTimelineResources = React.useCallback((medplum, resourceType, id) => {
5390
+ const ref = `${resourceType}/${id}`;
5391
+ const _count = 100;
5331
5392
  return Promise.allSettled([
5332
5393
  medplum.readHistory('Patient', id),
5333
- medplum.search('Communication', 'subject=Patient/' + id),
5334
- medplum.search('Device', 'patient=Patient/' + id),
5335
- medplum.search('DeviceRequest', 'patient=Patient/' + id),
5336
- medplum.search('DiagnosticReport', 'subject=Patient/' + id),
5337
- medplum.search('Media', 'subject=Patient/' + id),
5338
- medplum.search('ServiceRequest', 'subject=Patient/' + id),
5394
+ medplum.search('Communication', { subject: ref, _count }),
5395
+ medplum.search('Device', { patient: ref, _count }),
5396
+ medplum.search('DeviceRequest', { patient: ref, _count }),
5397
+ medplum.search('DiagnosticReport', { subject: ref, _count }),
5398
+ medplum.search('Media', { subject: ref, _count }),
5399
+ medplum.search('ServiceRequest', { subject: ref, _count }),
5400
+ medplum.search('Task', { subject: ref, _count }),
5339
5401
  ]);
5340
5402
  }, []);
5341
5403
  return (React.createElement(ResourceTimeline, { value: props.patient, loadTimelineResources: loadTimelineResources, createCommunication: (resource, sender, text) => ({
@@ -6925,12 +6987,15 @@
6925
6987
  }
6926
6988
 
6927
6989
  function ServiceRequestTimeline(props) {
6928
- return (React.createElement(ResourceTimeline, { value: props.serviceRequest, loadTimelineResources: async (medplum, _resourceType, id) => {
6990
+ return (React.createElement(ResourceTimeline, { value: props.serviceRequest, loadTimelineResources: async (medplum, resourceType, id) => {
6991
+ const ref = `${resourceType}/${id}`;
6992
+ const _count = 100;
6929
6993
  return Promise.allSettled([
6930
6994
  medplum.readHistory('ServiceRequest', id),
6931
- medplum.search('Communication', 'based-on=ServiceRequest/' + id),
6932
- medplum.search('Media', '_count=100&based-on=ServiceRequest/' + id),
6933
- medplum.search('DiagnosticReport', 'based-on=ServiceRequest/' + id),
6995
+ medplum.search('Communication', { 'based-on': ref, _count }),
6996
+ medplum.search('DiagnosticReport', { 'based-on': ref, _count }),
6997
+ medplum.search('Media', { 'based-on': ref, _count }),
6998
+ medplum.search('Task', { _filter: `based-on eq ${ref} or focus eq ${ref} or subject eq ${ref}`, _count }),
6934
6999
  ]);
6935
7000
  }, createCommunication: (resource, sender, text) => ({
6936
7001
  resourceType: 'Communication',
@@ -7240,7 +7305,7 @@
7240
7305
  React.createElement(core$1.Center, { sx: { flexDirection: 'column' } }, children),
7241
7306
  React.createElement(OperationOutcomeAlert, { issues: issues }),
7242
7307
  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') })),
7308
+ React.createElement(core$1.PasswordInput, { name: "password", label: "Password", autoComplete: "off", required: true, autoFocus: true, error: getErrorsForInput(outcome, 'password') })),
7244
7309
  React.createElement(core$1.Group, { position: "apart", mt: "xl", spacing: 0, noWrap: true },
7245
7310
  onForgotPassword && (React.createElement(core$1.Anchor, { component: "button", type: "button", color: "dimmed", onClick: onForgotPassword, size: "xs" }, "Forgot password")),
7246
7311
  React.createElement(core$1.Checkbox, { id: "remember", name: "remember", label: "Remember me", size: "xs", sx: { lineHeight: 1 } }),