@medplum/react 0.9.34 → 0.9.35

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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/SearchControlField.d.ts +2 -3
  3. package/dist/cjs/SearchFieldEditor.d.ts +1 -2
  4. package/dist/cjs/SearchFilterEditor.d.ts +1 -2
  5. package/dist/cjs/SearchFilterValueDialog.d.ts +1 -2
  6. package/dist/cjs/SearchFilterValueInput.d.ts +0 -2
  7. package/dist/cjs/SearchPopupMenu.d.ts +1 -2
  8. package/dist/cjs/index.js +46 -31
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/index.min.js +1 -1
  11. package/dist/cjs/index.min.js.map +1 -1
  12. package/dist/cjs/styles.css +51 -51
  13. package/dist/esm/GoogleButton.js +1 -1
  14. package/dist/esm/GoogleButton.js.map +1 -1
  15. package/dist/esm/Popup.js +5 -1
  16. package/dist/esm/Popup.js.map +1 -1
  17. package/dist/esm/SearchControl.js +10 -14
  18. package/dist/esm/SearchControl.js.map +1 -1
  19. package/dist/esm/SearchControlField.d.ts +2 -3
  20. package/dist/esm/SearchControlField.js +6 -8
  21. package/dist/esm/SearchControlField.js.map +1 -1
  22. package/dist/esm/SearchFieldEditor.d.ts +1 -2
  23. package/dist/esm/SearchFieldEditor.js +2 -2
  24. package/dist/esm/SearchFieldEditor.js.map +1 -1
  25. package/dist/esm/SearchFilterEditor.d.ts +1 -2
  26. package/dist/esm/SearchFilterEditor.js +5 -6
  27. package/dist/esm/SearchFilterEditor.js.map +1 -1
  28. package/dist/esm/SearchFilterValueDialog.d.ts +1 -2
  29. package/dist/esm/SearchFilterValueDialog.js +1 -1
  30. package/dist/esm/SearchFilterValueDialog.js.map +1 -1
  31. package/dist/esm/SearchFilterValueInput.d.ts +0 -2
  32. package/dist/esm/SearchFilterValueInput.js +1 -1
  33. package/dist/esm/SearchFilterValueInput.js.map +1 -1
  34. package/dist/esm/SearchPopupMenu.d.ts +1 -2
  35. package/dist/esm/SearchPopupMenu.js.map +1 -1
  36. package/dist/esm/index.min.js +1 -1
  37. package/dist/esm/index.min.js.map +1 -1
  38. package/dist/esm/styles.css +51 -51
  39. package/dist/esm/utils/blame.js +1 -0
  40. package/dist/esm/utils/blame.js.map +1 -1
  41. package/dist/esm/utils/outcomes.js +19 -1
  42. package/dist/esm/utils/outcomes.js.map +1 -1
  43. package/package.json +11 -11
package/README.md CHANGED
@@ -4,7 +4,7 @@ The Medplum React Component Library provides many helpful components to quickly
4
4
 
5
5
  The Medplum SDK can be used with any compliant FHIR server. However, some advanced features are only available when paired with a Medplum server.
6
6
 
7
- Check out a live demo: [https://docs.medplum.com/storybook/index.html](https://docs.medplum.com/storybook/index.html)
7
+ Check out a live demo: <https://storybook.medplum.com/>
8
8
 
9
9
  ## Key Features
10
10
 
@@ -1,4 +1,4 @@
1
- import { IndexedStructureDefinition, SearchRequest } from '@medplum/core';
1
+ import { SearchRequest } from '@medplum/core';
2
2
  import { ElementDefinition, SearchParameter } from '@medplum/fhirtypes';
3
3
  /**
4
4
  * The SearchControlField type describes a field in the search control.
@@ -34,8 +34,7 @@ export interface SearchControlField {
34
34
  }
35
35
  /**
36
36
  * Returns the collection of field definitions for the search request.
37
- * @param typeSchema The schema for the resource type
38
37
  * @param search The search request definition.
39
38
  * @returns An array of field definitions.
40
39
  */
41
- export declare function getFieldDefinitions(schema: IndexedStructureDefinition, search: SearchRequest): SearchControlField[];
40
+ export declare function getFieldDefinitions(search: SearchRequest): SearchControlField[];
@@ -1,7 +1,6 @@
1
1
  /// <reference types="react" />
2
- import { IndexedStructureDefinition, SearchRequest } from '@medplum/core';
2
+ import { SearchRequest } from '@medplum/core';
3
3
  interface SearchFieldEditorProps {
4
- schema: IndexedStructureDefinition;
5
4
  visible: boolean;
6
5
  search: SearchRequest;
7
6
  onOk: (search: SearchRequest) => void;
@@ -1,8 +1,7 @@
1
1
  /// <reference types="react" />
2
- import { IndexedStructureDefinition, SearchRequest } from '@medplum/core';
2
+ import { SearchRequest } from '@medplum/core';
3
3
  import './SearchFilterEditor.css';
4
4
  export interface SearchFilterEditorProps {
5
- schema: IndexedStructureDefinition;
6
5
  visible: boolean;
7
6
  search: SearchRequest;
8
7
  onOk: (search: SearchRequest) => void;
@@ -1,10 +1,9 @@
1
1
  /// <reference types="react" />
2
- import { Filter, IndexedStructureDefinition } from '@medplum/core';
2
+ import { Filter } from '@medplum/core';
3
3
  import { SearchParameter } from '@medplum/fhirtypes';
4
4
  export interface SearchFilterValueDialogProps {
5
5
  title: string;
6
6
  visible: boolean;
7
- schema: IndexedStructureDefinition;
8
7
  resourceType: string;
9
8
  searchParam?: SearchParameter;
10
9
  filter?: Filter;
@@ -1,8 +1,6 @@
1
1
  /// <reference types="react" />
2
- import { IndexedStructureDefinition } from '@medplum/core';
3
2
  import { SearchParameter } from '@medplum/fhirtypes';
4
3
  export interface SearchFilterValueInputProps {
5
- schema: IndexedStructureDefinition;
6
4
  resourceType: string;
7
5
  searchParam: SearchParameter;
8
6
  defaultValue?: string;
@@ -1,8 +1,7 @@
1
1
  /// <reference types="react" />
2
- import { Filter, IndexedStructureDefinition, SearchRequest } from '@medplum/core';
2
+ import { Filter, SearchRequest } from '@medplum/core';
3
3
  import { SearchParameter } from '@medplum/fhirtypes';
4
4
  export interface SearchPopupMenuProps {
5
- schema: IndexedStructureDefinition;
6
5
  search: SearchRequest;
7
6
  visible: boolean;
8
7
  x: number;
package/dist/cjs/index.js CHANGED
@@ -18,7 +18,25 @@
18
18
 
19
19
  function getIssuesForExpression(outcome, expression) {
20
20
  var _a;
21
- return (_a = outcome === null || outcome === void 0 ? void 0 : outcome.issue) === null || _a === void 0 ? void 0 : _a.filter((issue) => { var _a; return ((_a = issue.expression) === null || _a === void 0 ? void 0 : _a[0]) === expression; });
21
+ return (_a = outcome === null || outcome === void 0 ? void 0 : outcome.issue) === null || _a === void 0 ? void 0 : _a.filter((issue) => { var _a; return isExpressionMatch((_a = issue.expression) === null || _a === void 0 ? void 0 : _a[0], expression); });
22
+ }
23
+ function isExpressionMatch(expr1, expr2) {
24
+ // Expression can be either "fieldName" or "resourceType.fieldName"
25
+ if (expr1 === expr2) {
26
+ return true;
27
+ }
28
+ if (!expr1 || !expr2) {
29
+ return false;
30
+ }
31
+ const dot1 = expr1.indexOf('.');
32
+ if (dot1 >= 0 && expr1.substring(dot1 + 1) === expr2) {
33
+ return true;
34
+ }
35
+ const dot2 = expr2.indexOf('.');
36
+ if (dot2 >= 0 && expr2.substring(dot2 + 1) === expr1) {
37
+ return true;
38
+ }
39
+ return false;
22
40
  }
23
41
 
24
42
  function Input(props) {
@@ -518,7 +536,7 @@
518
536
  return clientId;
519
537
  }
520
538
  const origin = window.location.protocol + '//' + window.location.host;
521
- const authorizedOrigins = (_b = (_a = "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") === null || _a === void 0 ? void 0 : _a.split(',')) !== null && _b !== void 0 ? _b : [];
539
+ const authorizedOrigins = (_b = (_a = "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") === null || _a === void 0 ? void 0 : _a.split(',')) !== null && _b !== void 0 ? _b : [];
522
540
  if (authorizedOrigins.includes(origin)) {
523
541
  return "921088377005-3j1sa10vr6hj86jgmdfh2l53v3mp7lfi.apps.googleusercontent.com";
524
542
  }
@@ -2550,11 +2568,14 @@
2550
2568
  ((_b = propsRef.current) === null || _b === void 0 ? void 0 : _b.autoClose) &&
2551
2569
  (ref === null || ref === void 0 ? void 0 : ref.current) &&
2552
2570
  !ref.current.contains(e.target)) {
2571
+ killEvent(e);
2553
2572
  props.onClose();
2554
2573
  }
2555
2574
  }
2556
2575
  document.addEventListener('click', handleClick, true);
2557
- return () => document.removeEventListener('click', handleClick, true);
2576
+ return () => {
2577
+ document.removeEventListener('click', handleClick, true);
2578
+ };
2558
2579
  }, [props]);
2559
2580
  // Listen for changes in the location
2560
2581
  // If the browser navigates to a new page, close the popup
@@ -2956,27 +2977,25 @@
2956
2977
 
2957
2978
  /**
2958
2979
  * Returns the collection of field definitions for the search request.
2959
- * @param typeSchema The schema for the resource type
2960
2980
  * @param search The search request definition.
2961
2981
  * @returns An array of field definitions.
2962
2982
  */
2963
- function getFieldDefinitions(schema, search) {
2983
+ function getFieldDefinitions(search) {
2964
2984
  const resourceType = search.resourceType;
2965
2985
  const fields = [];
2966
2986
  for (const name of search.fields || ['id', '_lastUpdated']) {
2967
- fields.push(getFieldDefinition(schema, resourceType, name));
2987
+ fields.push(getFieldDefinition(resourceType, name));
2968
2988
  }
2969
2989
  return fields;
2970
2990
  }
2971
2991
  /**
2972
2992
  * Return the field definition for a given field name.
2973
2993
  * Field names can be either property names or search parameter codes.
2974
- * @param typeSchema The schema for the resource type
2975
2994
  * @param resourceType The resource type.
2976
2995
  * @param name The search field name (either property name or search parameter code).
2977
2996
  * @returns The field definition.
2978
2997
  */
2979
- function getFieldDefinition(schema, resourceType, name) {
2998
+ function getFieldDefinition(resourceType, name) {
2980
2999
  var _a;
2981
3000
  if (name === '_lastUpdated') {
2982
3001
  return {
@@ -3008,7 +3027,7 @@
3008
3027
  ],
3009
3028
  };
3010
3029
  }
3011
- const typeSchema = schema.types[resourceType];
3030
+ const typeSchema = core.globalSchema.types[resourceType];
3012
3031
  const exactElementDefinition = typeSchema.properties[name];
3013
3032
  const exactSearchParam = (_a = typeSchema.searchParams) === null || _a === void 0 ? void 0 : _a[name.toLowerCase()];
3014
3033
  // Best case: Exact match of element definition or search parameter.
@@ -3039,7 +3058,7 @@
3039
3058
  // Patient.email is a search parameter for the Patient.telecom element.
3040
3059
  // So we need to walk backwards to find the element definition.
3041
3060
  if (exactSearchParam) {
3042
- const details = core.getSearchParameterDetails(schema, resourceType, exactSearchParam);
3061
+ const details = core.getSearchParameterDetails(resourceType, exactSearchParam);
3043
3062
  return { name, elementDefinition: details.elementDefinition, searchParams: [exactSearchParam] };
3044
3063
  }
3045
3064
  // Worst case: no element definition and no search parameter.
@@ -3655,7 +3674,7 @@
3655
3674
  return null;
3656
3675
  }
3657
3676
  const resourceType = props.search.resourceType;
3658
- const typeDef = props.schema.types[resourceType];
3677
+ const typeDef = core.globalSchema.types[resourceType];
3659
3678
  const selected = (_a = state.search.fields) !== null && _a !== void 0 ? _a : [];
3660
3679
  const available = getFieldsList(typeDef)
3661
3680
  .filter((field) => !(selected === null || selected === void 0 ? void 0 : selected.includes(field)))
@@ -3730,7 +3749,7 @@
3730
3749
 
3731
3750
  function SearchFilterValueInput(props) {
3732
3751
  var _a;
3733
- const details = core.getSearchParameterDetails(props.schema, props.resourceType, props.searchParam);
3752
+ const details = core.getSearchParameterDetails(props.resourceType, props.searchParam);
3734
3753
  const name = 'filter-value';
3735
3754
  switch (details.type) {
3736
3755
  case core.SearchParameterType.REFERENCE:
@@ -3791,9 +3810,8 @@
3791
3810
  if (!props.visible) {
3792
3811
  return null;
3793
3812
  }
3794
- const schema = props.schema;
3795
3813
  const resourceType = props.search.resourceType;
3796
- const searchParams = schema.types[resourceType].searchParams;
3814
+ const searchParams = core.globalSchema.types[resourceType].searchParams;
3797
3815
  const filters = search.filters || [];
3798
3816
  return (React__default["default"].createElement(Dialog, { title: "Filters", visible: props.visible, onOk: () => props.onOk(searchRef.current), onCancel: props.onCancel },
3799
3817
  React__default["default"].createElement("div", { className: "medplum-filter-editor" },
@@ -3812,7 +3830,7 @@
3812
3830
  React__default["default"].createElement("tbody", null,
3813
3831
  filters.map((filter, index) => {
3814
3832
  if (index === editingIndex) {
3815
- return (React__default["default"].createElement(FilterRowInput, { key: `filter-${index}-${filters.length}-input`, schema: schema, resourceType: resourceType, searchParams: searchParams, defaultValue: filter, okText: "Save", onOk: (newFilter) => {
3833
+ return (React__default["default"].createElement(FilterRowInput, { key: `filter-${index}-${filters.length}-input`, resourceType: resourceType, searchParams: searchParams, defaultValue: filter, okText: "Save", onOk: (newFilter) => {
3816
3834
  const newFilters = [...filters];
3817
3835
  newFilters[index] = newFilter;
3818
3836
  setSearch(setFilters(searchRef.current, newFilters));
@@ -3823,7 +3841,7 @@
3823
3841
  return (React__default["default"].createElement(FilterRowDisplay, { key: `filter-${index}-${filters.length}-display`, resourceType: resourceType, searchParams: searchParams, filter: filter, onEdit: () => setEditingIndex(index), onDelete: () => setSearch(deleteFilter(searchRef.current, index)) }));
3824
3842
  }
3825
3843
  }),
3826
- React__default["default"].createElement(FilterRowInput, { schema: schema, resourceType: resourceType, searchParams: searchParams, okText: "Add", onOk: onAddFilter }))))));
3844
+ React__default["default"].createElement(FilterRowInput, { resourceType: resourceType, searchParams: searchParams, okText: "Add", onOk: onAddFilter }))))));
3827
3845
  }
3828
3846
  function FilterRowDisplay(props) {
3829
3847
  const { filter } = props;
@@ -3860,7 +3878,7 @@
3860
3878
  React__default["default"].createElement("td", null, operators && (React__default["default"].createElement(Select, { testid: "filter-operation", defaultValue: value.operator, onChange: setFilterOperator },
3861
3879
  React__default["default"].createElement("option", { value: "" }),
3862
3880
  operators.map((operator) => (React__default["default"].createElement("option", { key: operator, value: operator }, getOpString(operator))))))),
3863
- React__default["default"].createElement("td", null, searchParam && value.operator && (React__default["default"].createElement(SearchFilterValueInput, { schema: props.schema, resourceType: props.resourceType, searchParam: searchParam, defaultValue: value.value, onChange: setFilterValue }))),
3881
+ React__default["default"].createElement("td", null, searchParam && value.operator && (React__default["default"].createElement(SearchFilterValueInput, { resourceType: props.resourceType, searchParam: searchParam, defaultValue: value.value, onChange: setFilterValue }))),
3864
3882
  React__default["default"].createElement("td", null,
3865
3883
  value.code && value.operator && value.value && (React__default["default"].createElement(Button, { size: "small", onClick: () => {
3866
3884
  props.onOk(valueRef.current);
@@ -3881,7 +3899,7 @@
3881
3899
  return (React__default["default"].createElement(Dialog, { title: props.title, visible: props.visible, onOk: onOk, onCancel: props.onCancel },
3882
3900
  React__default["default"].createElement("div", { style: { width: 500 } },
3883
3901
  React__default["default"].createElement(Form, { onSubmit: onOk },
3884
- React__default["default"].createElement(SearchFilterValueInput, { schema: props.schema, resourceType: props.resourceType, searchParam: props.searchParam, defaultValue: value, autoFocus: true, onChange: setValue })))));
3902
+ React__default["default"].createElement(SearchFilterValueInput, { resourceType: props.resourceType, searchParam: props.searchParam, defaultValue: value, autoFocus: true, onChange: setValue })))));
3885
3903
  }
3886
3904
 
3887
3905
  function MenuSeparator() {
@@ -4077,7 +4095,7 @@
4077
4095
  function SearchControl(props) {
4078
4096
  var _a, _b, _c, _d;
4079
4097
  const medplum = useMedplum();
4080
- const [schema, setSchema] = React.useState();
4098
+ const [schemaLoaded, setSchemaLoaded] = React.useState(false);
4081
4099
  const [outcome, setOutcome] = React.useState();
4082
4100
  const { search, onLoad } = props;
4083
4101
  const [state, setState] = React.useState({
@@ -4095,7 +4113,7 @@
4095
4113
  React.useEffect(() => {
4096
4114
  setOutcome(undefined);
4097
4115
  medplum
4098
- .search(search.resourceType, core.formatSearchQuery(Object.assign(Object.assign({}, search), { total: 'accurate' })))
4116
+ .search(search.resourceType, core.formatSearchQuery(Object.assign(Object.assign({}, search), { total: 'accurate', fields: undefined })))
4099
4117
  .then((response) => {
4100
4118
  setState(Object.assign(Object.assign({}, stateRef.current), { searchResponse: response }));
4101
4119
  if (onLoad) {
@@ -4189,19 +4207,15 @@
4189
4207
  React.useEffect(() => {
4190
4208
  medplum
4191
4209
  .requestSchema(props.search.resourceType)
4192
- .then((newSchema) => {
4193
- // The schema could have the same object identity,
4194
- // so need to use the spread operator to kick React re-render.
4195
- setSchema(Object.assign({}, newSchema));
4196
- })
4210
+ .then(() => setSchemaLoaded(true))
4197
4211
  .catch(console.log);
4198
4212
  }, [medplum, props.search.resourceType]);
4199
- const typeSchema = (_a = schema === null || schema === void 0 ? void 0 : schema.types) === null || _a === void 0 ? void 0 : _a[props.search.resourceType];
4213
+ const typeSchema = schemaLoaded && ((_a = core.globalSchema === null || core.globalSchema === void 0 ? void 0 : core.globalSchema.types) === null || _a === void 0 ? void 0 : _a[props.search.resourceType]);
4200
4214
  if (!typeSchema) {
4201
4215
  return React__default["default"].createElement(Loading, null);
4202
4216
  }
4203
4217
  const checkboxColumn = props.checkboxesEnabled;
4204
- const fields = getFieldDefinitions(schema, search);
4218
+ const fields = getFieldDefinitions(search);
4205
4219
  const resourceType = search.resourceType;
4206
4220
  const lastResult = state.searchResponse;
4207
4221
  const entries = lastResult === null || lastResult === void 0 ? void 0 : lastResult.entry;
@@ -4251,7 +4265,7 @@
4251
4265
  (resources === null || resources === void 0 ? void 0 : resources.length) === 0 && (React__default["default"].createElement("div", { "data-testid": "empty-search", className: "medplum-empty-search" }, "No results")),
4252
4266
  outcome && (React__default["default"].createElement("div", { "data-testid": "search-error", className: "medplum-empty-search" },
4253
4267
  React__default["default"].createElement("pre", { style: { textAlign: 'left' } }, JSON.stringify(outcome, undefined, 2)))),
4254
- React__default["default"].createElement(SearchPopupMenu, { schema: schema, search: props.search, visible: state.popupVisible, x: state.popupX, y: state.popupY, searchParams: state.popupSearchParams, onPrompt: (searchParam, filter) => {
4268
+ React__default["default"].createElement(SearchPopupMenu, { search: props.search, visible: state.popupVisible, x: state.popupX, y: state.popupY, searchParams: state.popupSearchParams, onPrompt: (searchParam, filter) => {
4255
4269
  setState(Object.assign(Object.assign({}, stateRef.current), { popupVisible: false, filterDialogVisible: true, filterDialogSearchParam: searchParam, filterDialogFilter: filter }));
4256
4270
  }, onChange: (result) => {
4257
4271
  emitSearchChange(result);
@@ -4259,19 +4273,19 @@
4259
4273
  }, onClose: () => {
4260
4274
  setState(Object.assign(Object.assign({}, stateRef.current), { popupVisible: false, popupSearchParams: undefined }));
4261
4275
  } }),
4262
- React__default["default"].createElement(SearchFieldEditor, { schema: schema, search: props.search, visible: stateRef.current.fieldEditorVisible, onOk: (result) => {
4276
+ React__default["default"].createElement(SearchFieldEditor, { search: props.search, visible: stateRef.current.fieldEditorVisible, onOk: (result) => {
4263
4277
  emitSearchChange(result);
4264
4278
  setState(Object.assign(Object.assign({}, stateRef.current), { fieldEditorVisible: false }));
4265
4279
  }, onCancel: () => {
4266
4280
  setState(Object.assign(Object.assign({}, stateRef.current), { fieldEditorVisible: false }));
4267
4281
  } }),
4268
- React__default["default"].createElement(SearchFilterEditor, { schema: schema, search: props.search, visible: stateRef.current.filterEditorVisible, onOk: (result) => {
4282
+ React__default["default"].createElement(SearchFilterEditor, { search: props.search, visible: stateRef.current.filterEditorVisible, onOk: (result) => {
4269
4283
  emitSearchChange(result);
4270
4284
  setState(Object.assign(Object.assign({}, stateRef.current), { filterEditorVisible: false }));
4271
4285
  }, onCancel: () => {
4272
4286
  setState(Object.assign(Object.assign({}, stateRef.current), { filterEditorVisible: false }));
4273
4287
  } }),
4274
- React__default["default"].createElement(SearchFilterValueDialog, { visible: stateRef.current.filterDialogVisible, title: 'Input', schema: schema, resourceType: resourceType, searchParam: state.filterDialogSearchParam, filter: state.filterDialogFilter, defaultValue: '', onOk: (filter) => {
4288
+ React__default["default"].createElement(SearchFilterValueDialog, { visible: stateRef.current.filterDialogVisible, title: 'Input', resourceType: resourceType, searchParam: state.filterDialogSearchParam, filter: state.filterDialogFilter, defaultValue: '', onOk: (filter) => {
4275
4289
  emitSearchChange(addFilter(props.search, filter.code, filter.operator, filter.value));
4276
4290
  setState(Object.assign(Object.assign({}, stateRef.current), { filterDialogVisible: false }));
4277
4291
  }, onCancel: () => {
@@ -5636,6 +5650,7 @@
5636
5650
  function blame(history) {
5637
5651
  // Convert to array of array of lines
5638
5652
  const versions = history.entry
5653
+ .filter((entry) => !!entry.resource)
5639
5654
  .map((entry) => {
5640
5655
  var _a;
5641
5656
  return ({