• 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

96.0
/packages/react/src/chat/ThreadInbox/ThreadDetail.tsx
1
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
2
// SPDX-License-Identifier: Apache-2.0
3
import {
4
  ActionIcon,
5
  Box,
6
  Button,
7
  Divider,
8
  Flex,
9
  Group,
10
  Menu,
11
  Paper,
12
  ScrollArea,
13
  Stack,
14
  Text,
15
  Tooltip,
16
} from '@mantine/core';
17
import { getReferenceString } from '@medplum/core';
18
import type { Communication, DocumentReference, Patient, Reference } from '@medplum/fhirtypes';
19
import { IconChevronDown, IconInfoCircle } from '@tabler/icons-react';
20
import type { JSX } from 'react';
21
import { PatientSummary } from '../../PatientSummary/PatientSummary';
22
import type { PatientSummarySectionConfig } from '../../PatientSummary/PatientSummary.types';
23
import { ThreadChat } from '../ThreadChat/ThreadChat';
24
import classes from './ThreadDetail.module.css';
25

26
/**
27
 * Props for the ThreadDetail component.
28
 * @param thread - The selected thread (parent Communication) to display.
29
 * @param showPatientSummary - Whether to show the patient summary sidebar.
30
 * @param sections - Optional sections configuration for the patient summary.
31
 * @param uploadEnabled - Whether to show the attachment upload button in the chat input.
32
 * @param onViewInDocuments - When provided, shows a "View in Documents" action on attachment messages that invokes this callback with the attachment's DocumentReference.
33
 * @param onStatusChange - Fired when the user changes the thread status from the header menu.
34
 * @param onOpenSettings - When provided, shows a Message Settings button in the header that invokes this callback.
35
 * @param onMessageSent - Fired with the created Communication after the user sends a message in the chat.
36
 */
37
export interface ThreadDetailProps {
38
  readonly thread: Communication;
39
  readonly showPatientSummary?: boolean;
40
  readonly sections?: PatientSummarySectionConfig[];
41
  readonly uploadEnabled?: boolean;
42
  readonly onViewInDocuments?: (reference: Reference<DocumentReference>) => void;
43
  readonly onStatusChange: (status: Communication['status']) => void;
44
  readonly onOpenSettings?: () => void;
45
  readonly onMessageSent?: (message: Communication) => void;
46
}
47

48
/**
49
 * ThreadDetail renders the detail pane of the ThreadInbox: the thread header with a
50
 * status menu, the chat thread, and an optional patient summary sidebar.
51
 * @param props - The ThreadDetail React props.
52
 * @returns The ThreadDetail React node.
53
 */
54
export function ThreadDetail(props: ThreadDetailProps): JSX.Element {
55
  const {
56
    thread,
57
    showPatientSummary = false,
15✔
58
    sections,
59
    uploadEnabled,
60
    onViewInDocuments,
61
    onStatusChange,
62
    onOpenSettings,
63
    onMessageSent,
64
  } = props;
15✔
65

66
  return (
15✔
67
    <>
68
      {/* Main chat area */}
69
      <Flex direction="column" style={{ flex: 1 }} h="100%" className={classes.rightBorder}>
70
        <Paper h="100%">
71
          <Stack h="100%" gap={0}>
72
            <Flex h={64} align="center" justify="space-between" p="md">
73
              <Text fw={800} truncate fz="lg">
74
                {thread.topic?.text ?? 'Messages'}
16✔
75
              </Text>
76

77
              <Group gap="xs">
78
                {onOpenSettings && (
29✔
79
                  <Tooltip label="Message Settings" position="bottom" openDelay={500}>
80
                    <ActionIcon
81
                      aria-label="Message settings"
82
                      variant="transparent"
83
                      radius="xl"
84
                      size={32}
85
                      className="outline-icon-button"
86
                      onClick={onOpenSettings}
87
                    >
88
                      <IconInfoCircle size={16} />
89
                    </ActionIcon>
90
                  </Tooltip>
91
                )}
92

93
                <Menu position="bottom-end" shadow="md">
94
                  <Menu.Target>
95
                    <Button
96
                      variant="light"
97
                      color={getStatusColor(thread.status)}
98
                      rightSection={thread.status === 'completed' ? undefined : <IconChevronDown size={16} />}
15✔
99
                      radius="xl"
100
                      size="sm"
101
                    >
102
                      {thread.status
103
                        .split('-')
104
                        .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
28✔
105
                        .join(' ')}
106
                    </Button>
107
                  </Menu.Target>
108

109
                  {thread.status !== 'completed' && (
29✔
110
                    <Menu.Dropdown>
NEW
111
                      <Menu.Item onClick={() => onStatusChange('completed')}>Completed</Menu.Item>
×
112
                    </Menu.Dropdown>
113
                  )}
114
                </Menu>
115
              </Group>
116
            </Flex>
117
            <Divider />
118
            <Box flex={1} h="100%">
119
              <ThreadChat
120
                key={`${getReferenceString(thread)}`}
121
                title={'Messages'}
122
                thread={thread}
123
                excludeHeader={true}
124
                uploadEnabled={uploadEnabled}
125
                onViewInDocuments={onViewInDocuments}
126
                onMessageSent={onMessageSent}
127
              />
128
            </Box>
129
          </Stack>
130
        </Paper>
131
      </Flex>
132

133
      {/* Right sidebar - Patient summary */}
134
      {thread.subject && showPatientSummary && (
31✔
135
        <Box w={300} h="100%">
136
          <ScrollArea p={0} h="100%" scrollbarSize={10} type="hover" scrollHideDelay={250}>
137
            <PatientSummary key={thread.id} patient={thread.subject as Reference<Patient>} sections={sections} />
138
          </ScrollArea>
139
        </Box>
140
      )}
141
    </>
142
  );
143
}
144

145
function getStatusColor(status: Communication['status']): string {
146
  if (status === 'completed') {
15✔
147
    return 'green';
1✔
148
  }
149
  if (status === 'stopped') {
14✔
150
    return 'red';
1✔
151
  }
152
  return 'blue';
13✔
153
}
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