@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.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 pendingOrderIds = React4.useRef(/* @__PURE__ */ new Set());
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 orderIds = Array.from(pendingOrderIds.current);
641
+ const keys = Array.from(pendingKeys.current);
632
642
  const resolvers = new Map(pendingResolvers.current);
633
- pendingOrderIds.current.clear();
643
+ pendingKeys.current.clear();
634
644
  pendingResolvers.current.clear();
635
645
  batchScheduled.current = false;
636
- if (orderIds.length === 0) return;
637
- try {
638
- const query = orderIds.map((id) => `order_ids[]=${encodeURIComponent(id)}`).join("&");
639
- const response = await fetch(`${apiBaseUrl}/api/natoe-colab/conversations/previews?${query}`, {
640
- headers: authHeaders()
641
- });
642
- if (!response.ok) throw new Error(`Preview batch failed: ${response.status}`);
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
- if (previewCache.current.has(orderId)) {
665
- return Promise.resolve(previewCache.current.get(orderId) ?? null);
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
- pendingOrderIds.current.add(orderId);
669
- const existing = pendingResolvers.current.get(orderId) || [];
692
+ pendingKeys.current.add(key);
693
+ const existing = pendingResolvers.current.get(key) || [];
670
694
  existing.push(resolve);
671
- pendingResolvers.current.set(orderId, existing);
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
- previewCache.current.delete(orderId);
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;