• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

medplum / medplum / 30566672722

30 Jul 2026 05:35PM UTC coverage: 92.022% (+0.03%) from 91.994%
30566672722

push

github

web-flow
Provider: Add Message Settings to Messages (#9621)

* feat(react): rework New Message dialog with MultiResourceInput

Replace the QuestionnaireForm-based practitioner picker with
MultiResourceInput, defaulting the signed-in provider. Make the patient
required and disable Next until a patient and at least one practitioner are
selected. Apply the shared Send-Fax-style modal spacing/font.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kevin Shaw <kevin@medplum.com>

* feat(react): add Message Settings dialog for threads

Add EditTopicDialog, opened from a new info-icon button in the thread header,
to edit a thread's topic and practitioner participants after creation. References
are resolved in parallel before render so all fields appear together, with a
failed patient read handled independently. Threads with no practitioner recipient
fall back to showing the sender practitioner. Non-practitioner recipients are
preserved on save.

Add useThreadInbox.updateThreadParent so an edited thread updates in place rather
than refetching, keeping a draft thread (no reply yet) in the list. Order the
inbox by latest message time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kevin Shaw <kevin@medplum.com>

* feat(react): auto-select first thread in inbox

When no thread is selected and the list has items, navigate to the first
thread, matching the Tasks/Faxes boards. Add an optional replace flag to the
Medplum navigate function (forwarded by the app router wrappers) so the implicit
selection replaces history instead of adding a back-button entry that would
re-trigger the selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kevin Shaw <kevin@medplum.com>

* fix(react): type resolved practitioners via discriminant narrowing

The full tsc build (not run by vitest/esbuild) rejected the explicit
`PromiseFulfilledResult<Practitioner>` type predicate because readReference
resolves to WithId<Prac... (continued)

22738 of 25819 branches covered (88.07%)

Branch coverage included in aggregate %.

103 of 113 new or added lines in 10 files covered. (91.15%)

1 existing line in 1 file now uncovered.

40309 of 42694 relevant lines covered (94.41%)

12266.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.16
/packages/react/src/chat/ThreadInbox/EditThreadDialog.tsx
1
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
// SPDX-License-Identifier: Apache-2.0
3
import { Box, Button, Modal, Stack } from '@mantine/core';
4
import { showNotification } from '@mantine/notifications';
5
import { isReference, normalizeErrorString } from '@medplum/core';
6
import type { Communication, Patient, Practitioner, Reference } from '@medplum/fhirtypes';
7
import { useMedplum, useResource } from '@medplum/react-hooks';
8
import type { JSX } from 'react';
9
import { useMemo, useState } from 'react';
10
import classes from './EditThreadDialog.module.css';
11
import { ThreadMessageForm } from './ThreadMessageForm';
12

13
/**
14
 * Props for the EditThreadDialog component.
15
 * @param thread - The thread (root Communication) to edit, as a resource or a reference to resolve.
16
 * @param opened - Whether the dialog is open.
17
 * @param onClose - Callback fired when the dialog is closed.
18
 * @param onSaved - Callback fired with the updated Communication resource after a successful save.
19
 */
20
export interface EditThreadDialogProps {
21
  thread: Communication | Reference<Communication>;
22
  opened: boolean;
23
  onClose: () => void;
24
  onSaved?: (communication: Communication) => void;
25
}
26

27
export const EditThreadDialog = (props: EditThreadDialogProps): JSX.Element => {
3✔
28
  const { opened, onClose, onSaved } = props;
25✔
29
  const medplum = useMedplum();
25✔
30
  const thread = useResource(props.thread);
25✔
31

32
  const patientRef = thread?.subject as Reference<Patient> | undefined;
25✔
33
  const initialPractitioners = useMemo(() => {
25✔
34
    const fromRecipients = (thread?.recipient ?? []).filter((r) => isReference<Practitioner>(r, 'Practitioner'));
13✔
35
    if (fromRecipients.length > 0) {
10✔
36
      return fromRecipients;
6✔
37
    }
38
    // Fallback for threads with no practitioner recipient (e.g. legacy/patient-only threads):
39
    // surface the sender when it is a Practitioner, so the field isn't empty. Saving then
40
    // migrates this practitioner into the recipient list.
41
    const sender = thread?.sender;
4✔
42
    if (isReference<Practitioner>(sender, 'Practitioner')) {
10✔
43
      return [sender];
2✔
44
    }
45
    return [];
2✔
46
  }, [thread?.recipient, thread?.sender]);
47

48
  // The dialog is mounted only while open (see ThreadInbox), so this state initializes fresh on
49
  // each open — edits dismissed without saving are abandoned with no leftover form state.
50
  const [topic, setTopic] = useState(thread?.topic?.text ?? '');
25✔
51
  const [practitioners, setPractitioners] = useState<Reference<Practitioner>[]>(initialPractitioners);
25✔
52

53
  // When the thread is passed as a reference it resolves after mount, so re-initialize
54
  // the form state when it arrives (React's "adjusting state when a prop changes" pattern).
55
  const [initializedThread, setInitializedThread] = useState(thread);
25✔
56
  if (thread !== initializedThread) {
25✔
57
    setInitializedThread(thread);
1✔
58
    setTopic(thread?.topic?.text ?? '');
1!
59
    setPractitioners(initialPractitioners);
1✔
60
  }
61

62
  const handleSave = async (): Promise<void> => {
25✔
63
    if (!thread) {
4!
NEW
64
      return;
×
65
    }
66
    // Preserve all non-Practitioner recipients (the patient subject, RelatedPerson,
67
    // CareTeam, etc.) and replace only the Practitioner entries with the edited set,
68
    // so editing the topic/practitioners never silently drops other participants.
69
    const preservedRecipients = (thread.recipient ?? []).filter((r) => !isReference<Practitioner>(r, 'Practitioner'));
5✔
70
    const updated: Communication = {
4✔
71
      ...thread,
72
      recipient: [...preservedRecipients, ...practitioners],
73
      topic: topic ? { text: topic } : undefined,
4!
74
    };
75

76
    try {
4✔
77
      const saved = await medplum.updateResource(updated);
4✔
78
      onSaved?.(saved);
3✔
79
      onClose();
4✔
80
    } catch (error) {
81
      showNotification({ color: 'red', message: normalizeErrorString(error) });
1✔
82
    }
83
  };
84

85
  return (
25✔
86
    <Modal opened={opened} onClose={onClose} title="Message Settings" size="md" classNames={classes}>
87
      {thread && (
49✔
88
        <Stack gap={0}>
89
          <Stack gap="lg" p="lg">
90
            <ThreadMessageForm
91
              defaultPractitioners={initialPractitioners}
92
              onPractitionersChange={setPractitioners}
93
              topic={topic}
94
              onTopicChange={setTopic}
95
              defaultPatient={patientRef}
96
            />
97
          </Stack>
98

99
          <Box px="lg" pb="lg">
100
            <Button w="100%" onClick={handleSave} disabled={practitioners.length === 0}>
101
              Save
102
            </Button>
103
          </Box>
104
        </Stack>
105
      )}
106
    </Modal>
107
  );
108
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc