@natoe/colab 0.1.27 → 0.1.28
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.
- package/dist/index.d.mts +52 -9
- package/dist/index.d.ts +52 -9
- package/dist/index.js +115 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +115 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -582,6 +582,16 @@ function applyThemeOverrides(theme) {
|
|
|
582
582
|
}
|
|
583
583
|
});
|
|
584
584
|
}
|
|
585
|
+
function previewKey(orderId, kind) {
|
|
586
|
+
return `${kind}|${orderId}`;
|
|
587
|
+
}
|
|
588
|
+
function parsePreviewKey(key) {
|
|
589
|
+
const separator = key.indexOf("|");
|
|
590
|
+
return {
|
|
591
|
+
kind: key.slice(0, separator),
|
|
592
|
+
orderId: key.slice(separator + 1)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
585
595
|
var CollabContext = React4.createContext(null);
|
|
586
596
|
function useCollab() {
|
|
587
597
|
const context = React4.useContext(CollabContext);
|
|
@@ -597,7 +607,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
597
607
|
});
|
|
598
608
|
const [unreadCounts, setUnreadCounts] = React4.useState({});
|
|
599
609
|
const [unreadCountsByOrder, setUnreadCountsByOrder] = React4.useState({});
|
|
600
|
-
const
|
|
610
|
+
const pendingKeys = React4.useRef(/* @__PURE__ */ new Set());
|
|
601
611
|
const pendingResolvers = React4.useRef(/* @__PURE__ */ new Map());
|
|
602
612
|
const previewCache = React4.useRef(/* @__PURE__ */ new Map());
|
|
603
613
|
const batchScheduled = React4.useRef(false);
|
|
@@ -628,47 +638,61 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
628
638
|
[config]
|
|
629
639
|
);
|
|
630
640
|
const flushPreviewBatch = React4.useCallback(async () => {
|
|
631
|
-
const
|
|
641
|
+
const keys = Array.from(pendingKeys.current);
|
|
632
642
|
const resolvers = new Map(pendingResolvers.current);
|
|
633
|
-
|
|
643
|
+
pendingKeys.current.clear();
|
|
634
644
|
pendingResolvers.current.clear();
|
|
635
645
|
batchScheduled.current = false;
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const data = snakeToCamel(await response.json());
|
|
644
|
-
orderIds.forEach((orderId) => {
|
|
645
|
-
const preview = data.previews[orderId] ?? null;
|
|
646
|
-
if (preview !== null) {
|
|
647
|
-
previewCache.current.set(orderId, preview);
|
|
648
|
-
}
|
|
649
|
-
resolvers.get(orderId)?.forEach((resolve) => resolve(preview));
|
|
650
|
-
});
|
|
651
|
-
} catch (error) {
|
|
652
|
-
config.onError?.({
|
|
653
|
-
code: "PREVIEW_BATCH_ERROR",
|
|
654
|
-
message: "Failed to fetch conversation previews",
|
|
655
|
-
details: error
|
|
656
|
-
});
|
|
657
|
-
orderIds.forEach((orderId) => {
|
|
658
|
-
resolvers.get(orderId)?.forEach((resolve) => resolve(null));
|
|
659
|
-
});
|
|
646
|
+
if (keys.length === 0) return;
|
|
647
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const key of keys) {
|
|
649
|
+
const { orderId, kind } = parsePreviewKey(key);
|
|
650
|
+
const list = byKind.get(kind) ?? [];
|
|
651
|
+
list.push(orderId);
|
|
652
|
+
byKind.set(kind, list);
|
|
660
653
|
}
|
|
654
|
+
await Promise.all(
|
|
655
|
+
Array.from(byKind.entries()).map(async ([kind, orderIds]) => {
|
|
656
|
+
try {
|
|
657
|
+
const query = orderIds.map((id) => `order_ids[]=${encodeURIComponent(id)}`).join("&");
|
|
658
|
+
const response = await fetch(
|
|
659
|
+
`${apiBaseUrl}/api/natoe-colab/conversations/previews?${query}&kind=${encodeURIComponent(kind)}`,
|
|
660
|
+
{ headers: authHeaders() }
|
|
661
|
+
);
|
|
662
|
+
if (!response.ok) throw new Error(`Preview batch failed: ${response.status}`);
|
|
663
|
+
const data = snakeToCamel(await response.json());
|
|
664
|
+
orderIds.forEach((orderId) => {
|
|
665
|
+
const key = previewKey(orderId, kind);
|
|
666
|
+
const preview = data.previews[orderId] ?? null;
|
|
667
|
+
if (preview !== null) {
|
|
668
|
+
previewCache.current.set(key, preview);
|
|
669
|
+
}
|
|
670
|
+
resolvers.get(key)?.forEach((resolve) => resolve(preview));
|
|
671
|
+
});
|
|
672
|
+
} catch (error) {
|
|
673
|
+
config.onError?.({
|
|
674
|
+
code: "PREVIEW_BATCH_ERROR",
|
|
675
|
+
message: "Failed to fetch conversation previews",
|
|
676
|
+
details: error
|
|
677
|
+
});
|
|
678
|
+
orderIds.forEach((orderId) => {
|
|
679
|
+
resolvers.get(previewKey(orderId, kind))?.forEach((resolve) => resolve(null));
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
);
|
|
661
684
|
}, [apiBaseUrl, authHeaders, config]);
|
|
662
685
|
const requestPreview = React4.useCallback(
|
|
663
|
-
(orderId) => {
|
|
664
|
-
|
|
665
|
-
|
|
686
|
+
(orderId, kind = "case") => {
|
|
687
|
+
const key = previewKey(orderId, kind);
|
|
688
|
+
if (previewCache.current.has(key)) {
|
|
689
|
+
return Promise.resolve(previewCache.current.get(key) ?? null);
|
|
666
690
|
}
|
|
667
691
|
return new Promise((resolve) => {
|
|
668
|
-
|
|
669
|
-
const existing = pendingResolvers.current.get(
|
|
692
|
+
pendingKeys.current.add(key);
|
|
693
|
+
const existing = pendingResolvers.current.get(key) || [];
|
|
670
694
|
existing.push(resolve);
|
|
671
|
-
pendingResolvers.current.set(
|
|
695
|
+
pendingResolvers.current.set(key, existing);
|
|
672
696
|
if (!batchScheduled.current) {
|
|
673
697
|
batchScheduled.current = true;
|
|
674
698
|
queueMicrotask(flushPreviewBatch);
|
|
@@ -677,8 +701,16 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
677
701
|
},
|
|
678
702
|
[flushPreviewBatch]
|
|
679
703
|
);
|
|
680
|
-
const invalidatePreview = React4.useCallback((orderId) => {
|
|
681
|
-
|
|
704
|
+
const invalidatePreview = React4.useCallback((orderId, kind) => {
|
|
705
|
+
if (kind) {
|
|
706
|
+
previewCache.current.delete(previewKey(orderId, kind));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
for (const key of Array.from(previewCache.current.keys())) {
|
|
710
|
+
if (parsePreviewKey(key).orderId === orderId) {
|
|
711
|
+
previewCache.current.delete(key);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
682
714
|
}, []);
|
|
683
715
|
const fetchMessages = React4.useCallback(
|
|
684
716
|
async (conversationId, options = {}) => {
|
|
@@ -705,11 +737,17 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
705
737
|
message: "Cannot create conversation: orderId is empty. Host app passed an order with no orderId/mainOrderId set."
|
|
706
738
|
};
|
|
707
739
|
}
|
|
740
|
+
const kind = options.kind ?? "case";
|
|
708
741
|
const response = await fetch(`${apiBaseUrl}/api/natoe-colab/orders/${orderId}/messages`, {
|
|
709
742
|
method: "POST",
|
|
710
743
|
headers: authHeaders(),
|
|
711
744
|
body: JSON.stringify({
|
|
712
745
|
message,
|
|
746
|
+
// Top-level, matching the backend's `params["kind"]`. It selects
|
|
747
|
+
// BOTH which thread is addressed and which authorization path runs
|
|
748
|
+
// — help kinds go through Access.can_access_help?/5, not the
|
|
749
|
+
// broader case-chat check.
|
|
750
|
+
kind,
|
|
713
751
|
conversation: {
|
|
714
752
|
name: options.name,
|
|
715
753
|
participant_ids: options.participantIds ?? []
|
|
@@ -721,7 +759,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
721
759
|
throw { code: "CREATE_ERROR", message: "Failed to create conversation", details: error };
|
|
722
760
|
}
|
|
723
761
|
const data = snakeToCamel(await response.json());
|
|
724
|
-
previewCache.current.delete(orderId);
|
|
762
|
+
previewCache.current.delete(previewKey(orderId, kind));
|
|
725
763
|
return data;
|
|
726
764
|
},
|
|
727
765
|
[apiBaseUrl, authHeaders]
|
|
@@ -855,7 +893,8 @@ function useConversation({
|
|
|
855
893
|
orderId,
|
|
856
894
|
patientData,
|
|
857
895
|
participantIds = [],
|
|
858
|
-
loadHistory = true
|
|
896
|
+
loadHistory = true,
|
|
897
|
+
kind = "case"
|
|
859
898
|
}) {
|
|
860
899
|
const {
|
|
861
900
|
socket,
|
|
@@ -983,7 +1022,7 @@ function useConversation({
|
|
|
983
1022
|
setIsLoading(true);
|
|
984
1023
|
setError(null);
|
|
985
1024
|
try {
|
|
986
|
-
const preview = await requestPreview(orderId);
|
|
1025
|
+
const preview = await requestPreview(orderId, kind);
|
|
987
1026
|
if (cancelled) return;
|
|
988
1027
|
if (!preview) {
|
|
989
1028
|
setConversation(null);
|
|
@@ -1040,7 +1079,7 @@ function useConversation({
|
|
|
1040
1079
|
markedReadIdsRef.current.clear();
|
|
1041
1080
|
setIsConnected(false);
|
|
1042
1081
|
};
|
|
1043
|
-
}, [orderId]);
|
|
1082
|
+
}, [orderId, kind]);
|
|
1044
1083
|
const ensureConversation = React4.useCallback(
|
|
1045
1084
|
async (payload) => {
|
|
1046
1085
|
if (conversation) return { conv: conversation, persistedByCreate: false };
|
|
@@ -1051,13 +1090,14 @@ function useConversation({
|
|
|
1051
1090
|
const inflight = (async () => {
|
|
1052
1091
|
const result = await createConversationWithMessage(orderId, payload, {
|
|
1053
1092
|
name: buildName(),
|
|
1054
|
-
participantIds
|
|
1093
|
+
participantIds,
|
|
1094
|
+
kind
|
|
1055
1095
|
});
|
|
1056
1096
|
setConversation(result.conversation);
|
|
1057
1097
|
setParticipants(result.conversation.participants);
|
|
1058
1098
|
setMessages([result.message]);
|
|
1059
1099
|
joinChannel(result.conversation);
|
|
1060
|
-
invalidatePreview(orderId);
|
|
1100
|
+
invalidatePreview(orderId, kind);
|
|
1061
1101
|
if (loadHistory) {
|
|
1062
1102
|
fetchMessages(result.conversation.id).then((history) => {
|
|
1063
1103
|
setMessages((prev) => {
|
|
@@ -1087,6 +1127,7 @@ function useConversation({
|
|
|
1087
1127
|
conversation,
|
|
1088
1128
|
createConversationWithMessage,
|
|
1089
1129
|
orderId,
|
|
1130
|
+
kind,
|
|
1090
1131
|
buildName,
|
|
1091
1132
|
participantIds,
|
|
1092
1133
|
joinChannel,
|
|
@@ -4496,6 +4537,7 @@ function CollabPanel({
|
|
|
4496
4537
|
orderId,
|
|
4497
4538
|
patientData,
|
|
4498
4539
|
participantIds,
|
|
4540
|
+
kind,
|
|
4499
4541
|
showSeenBy = true,
|
|
4500
4542
|
onBack,
|
|
4501
4543
|
hidePatientName = false,
|
|
@@ -4534,7 +4576,7 @@ function CollabPanel({
|
|
|
4534
4576
|
loadMoreMessages,
|
|
4535
4577
|
pinMessage,
|
|
4536
4578
|
unpinMessage
|
|
4537
|
-
} = useConversation({ orderId, patientData, participantIds });
|
|
4579
|
+
} = useConversation({ orderId, patientData, participantIds, kind });
|
|
4538
4580
|
React4.useEffect(() => {
|
|
4539
4581
|
onConversationChange?.(conversation);
|
|
4540
4582
|
}, [conversation, onConversationChange]);
|
|
@@ -4753,6 +4795,7 @@ function CollabPopup({
|
|
|
4753
4795
|
orderId,
|
|
4754
4796
|
patientData,
|
|
4755
4797
|
participantIds,
|
|
4798
|
+
kind,
|
|
4756
4799
|
isOpen,
|
|
4757
4800
|
onClose,
|
|
4758
4801
|
onBack,
|
|
@@ -4941,6 +4984,7 @@ function CollabPopup({
|
|
|
4941
4984
|
orderId,
|
|
4942
4985
|
patientData,
|
|
4943
4986
|
participantIds,
|
|
4987
|
+
kind,
|
|
4944
4988
|
hidePatientName: true,
|
|
4945
4989
|
onConversationChange: setLoadedConversation,
|
|
4946
4990
|
showSettings,
|
|
@@ -5058,7 +5102,8 @@ function useInlineCollab({
|
|
|
5058
5102
|
orderId,
|
|
5059
5103
|
patientData,
|
|
5060
5104
|
participantIds = [],
|
|
5061
|
-
messageLimit = 5
|
|
5105
|
+
messageLimit = 5,
|
|
5106
|
+
kind = "case"
|
|
5062
5107
|
}) {
|
|
5063
5108
|
const {
|
|
5064
5109
|
socket,
|
|
@@ -5106,7 +5151,7 @@ function useInlineCollab({
|
|
|
5106
5151
|
setIsLoading(true);
|
|
5107
5152
|
setError(null);
|
|
5108
5153
|
try {
|
|
5109
|
-
const result = await requestPreview(orderId);
|
|
5154
|
+
const result = await requestPreview(orderId, kind);
|
|
5110
5155
|
if (cancelled) return;
|
|
5111
5156
|
setPreview(result);
|
|
5112
5157
|
if (result) {
|
|
@@ -5202,7 +5247,8 @@ function useInlineCollab({
|
|
|
5202
5247
|
try {
|
|
5203
5248
|
const result = await createConversationWithMessage(orderId, payload, {
|
|
5204
5249
|
name: buildName(),
|
|
5205
|
-
participantIds
|
|
5250
|
+
participantIds,
|
|
5251
|
+
kind
|
|
5206
5252
|
});
|
|
5207
5253
|
const newPreview = {
|
|
5208
5254
|
conversationId: result.conversation.id,
|
|
@@ -5216,7 +5262,7 @@ function useInlineCollab({
|
|
|
5216
5262
|
setPreview(newPreview);
|
|
5217
5263
|
setMessages([result.message]);
|
|
5218
5264
|
setParticipants(result.conversation.participants);
|
|
5219
|
-
invalidatePreview(orderId);
|
|
5265
|
+
invalidatePreview(orderId, kind);
|
|
5220
5266
|
if (elementRef.current) {
|
|
5221
5267
|
subscribe(result.conversation.id);
|
|
5222
5268
|
}
|
|
@@ -5341,6 +5387,7 @@ function CollabInline({
|
|
|
5341
5387
|
orderId,
|
|
5342
5388
|
patientData,
|
|
5343
5389
|
participantIds,
|
|
5390
|
+
kind,
|
|
5344
5391
|
onExpand,
|
|
5345
5392
|
messageLimit = 1,
|
|
5346
5393
|
placeholder = "Discuss this case\u2026",
|
|
@@ -5359,7 +5406,7 @@ function CollabInline({
|
|
|
5359
5406
|
sendMessage,
|
|
5360
5407
|
sendAudioMessage,
|
|
5361
5408
|
containerRef
|
|
5362
|
-
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
|
|
5409
|
+
} = useInlineCollab({ orderId, patientData, participantIds, messageLimit, kind });
|
|
5363
5410
|
const containerStyle = {
|
|
5364
5411
|
...styles14.container,
|
|
5365
5412
|
backgroundColor: pal.bg,
|
|
@@ -5851,6 +5898,11 @@ function useConversationList(options) {
|
|
|
5851
5898
|
totalUnread
|
|
5852
5899
|
};
|
|
5853
5900
|
}
|
|
5901
|
+
|
|
5902
|
+
// src/core/types.ts
|
|
5903
|
+
function isHelpKind(kind) {
|
|
5904
|
+
return kind === "help_lab" || kind === "help_radiologist";
|
|
5905
|
+
}
|
|
5854
5906
|
function ConversationListItem({
|
|
5855
5907
|
item,
|
|
5856
5908
|
isSelected,
|
|
@@ -5887,6 +5939,10 @@ function ConversationListItem({
|
|
|
5887
5939
|
children: displayName
|
|
5888
5940
|
}
|
|
5889
5941
|
),
|
|
5942
|
+
isHelpKind(item.kind) && /* A case can show two rows with the same patient name — the shared
|
|
5943
|
+
case thread and a private help thread. Without this the two are
|
|
5944
|
+
indistinguishable in the inbox. */
|
|
5945
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles15.helpPill, "aria-label": "Help thread", children: "Help" }),
|
|
5890
5946
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5891
5947
|
"span",
|
|
5892
5948
|
{
|
|
@@ -6033,6 +6089,17 @@ var styles15 = {
|
|
|
6033
6089
|
nameUnread: {
|
|
6034
6090
|
fontWeight: FONT_WEIGHT.bold
|
|
6035
6091
|
},
|
|
6092
|
+
helpPill: {
|
|
6093
|
+
fontSize: FONT_SIZE.xs,
|
|
6094
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
6095
|
+
color: COLOR.primary,
|
|
6096
|
+
background: COLOR.primaryBg,
|
|
6097
|
+
borderRadius: RADIUS.sm,
|
|
6098
|
+
padding: `0 ${SPACE.S2}`,
|
|
6099
|
+
lineHeight: "16px",
|
|
6100
|
+
flexShrink: 0,
|
|
6101
|
+
whiteSpace: "nowrap"
|
|
6102
|
+
},
|
|
6036
6103
|
time: {
|
|
6037
6104
|
fontSize: FONT_SIZE.xs,
|
|
6038
6105
|
color: COLOR.neutral500,
|
|
@@ -6807,6 +6874,7 @@ exports.THEME_DEFAULTS = THEME_DEFAULTS;
|
|
|
6807
6874
|
exports.THEME_VAR = THEME_VAR;
|
|
6808
6875
|
exports.TYPING_DEBOUNCE_MS = TYPING_DEBOUNCE_MS;
|
|
6809
6876
|
exports.applyThemeOverrides = applyThemeOverrides;
|
|
6877
|
+
exports.isHelpKind = isHelpKind;
|
|
6810
6878
|
exports.useAudioRecorder = useAudioRecorder;
|
|
6811
6879
|
exports.useChannelSettings = useChannelSettings;
|
|
6812
6880
|
exports.useCollab = useCollab;
|