@droppii-org/chat-sdk 0.7.18 → 0.7.21

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 (50) hide show
  1. package/dist/components/conversation/DeskBackgroundSyncStatus.d.ts +3 -1
  2. package/dist/components/conversation/DeskBackgroundSyncStatus.d.ts.map +1 -1
  3. package/dist/components/conversation/DeskBackgroundSyncStatus.js +3 -2
  4. package/dist/components/conversation/ThreadCard.d.ts +3 -1
  5. package/dist/components/conversation/ThreadCard.d.ts.map +1 -1
  6. package/dist/components/conversation/ThreadCard.js +3 -3
  7. package/dist/components/conversation-detail/ConversationDetailBootStatus.d.ts +4 -0
  8. package/dist/components/conversation-detail/ConversationDetailBootStatus.d.ts.map +1 -0
  9. package/dist/components/conversation-detail/ConversationDetailBootStatus.js +15 -0
  10. package/dist/components/message/footer/MediaActions.d.ts +2 -1
  11. package/dist/components/message/footer/MediaActions.d.ts.map +1 -1
  12. package/dist/components/message/footer/MediaActions.js +2 -2
  13. package/dist/components/message/footer/MediaUploadButtons.d.ts +2 -1
  14. package/dist/components/message/footer/MediaUploadButtons.d.ts.map +1 -1
  15. package/dist/components/message/footer/MediaUploadButtons.js +15 -3
  16. package/dist/components/rich-text-editor/RichTextEditor.d.ts +3 -1
  17. package/dist/components/rich-text-editor/RichTextEditor.d.ts.map +1 -1
  18. package/dist/components/rich-text-editor/RichTextEditor.js +2 -2
  19. package/dist/components/thread/SessionDetailCard.d.ts.map +1 -1
  20. package/dist/components/thread/SessionDetailCard.js +31 -40
  21. package/dist/components/thread/SessionLabelRow.d.ts +15 -0
  22. package/dist/components/thread/SessionLabelRow.d.ts.map +1 -0
  23. package/dist/components/thread/SessionLabelRow.js +53 -0
  24. package/dist/components/thread/SessionSection.d.ts +2 -1
  25. package/dist/components/thread/SessionSection.d.ts.map +1 -1
  26. package/dist/components/thread/SessionSection.js +2 -2
  27. package/dist/components/thread/SessionTagPicker.d.ts.map +1 -1
  28. package/dist/components/thread/SessionTagPicker.js +2 -5
  29. package/dist/components/thread/SupportSessionPanel.d.ts.map +1 -1
  30. package/dist/components/thread/SupportSessionPanel.js +17 -15
  31. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  32. package/dist/hooks/global/useGlobalEvent.js +4 -2
  33. package/dist/hooks/session/useGetListSessionByConversation.d.ts +2 -2
  34. package/dist/hooks/session/useGetListSessionByConversation.d.ts.map +1 -1
  35. package/dist/hooks/session/useGetListSessionByConversation.js +14 -8
  36. package/dist/hooks/session/useSessionLabelsAutosave.d.ts +25 -0
  37. package/dist/hooks/session/useSessionLabelsAutosave.d.ts.map +1 -0
  38. package/dist/hooks/session/useSessionLabelsAutosave.js +90 -0
  39. package/dist/hooks/session/useUpdateSessionInfo.d.ts +4 -2
  40. package/dist/hooks/session/useUpdateSessionInfo.d.ts.map +1 -1
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -0
  44. package/dist/locales/vi/common.json +3 -3
  45. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  46. package/dist/screens/conversation-detail/index.js +4 -1
  47. package/dist/styles/global.css +1 -1
  48. package/dist/types/dto.d.ts +8 -2
  49. package/dist/types/dto.d.ts.map +1 -1
  50. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  /** Under the list header while a fresh device's first sync keeps pulling the other conversations. */
2
- declare const DeskBackgroundSyncStatus: () => import("react/jsx-runtime").JSX.Element | null;
2
+ declare const DeskBackgroundSyncStatus: ({ className }: {
3
+ className?: string;
4
+ }) => import("react/jsx-runtime").JSX.Element | null;
3
5
  export default DeskBackgroundSyncStatus;
4
6
  //# sourceMappingURL=DeskBackgroundSyncStatus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DeskBackgroundSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskBackgroundSyncStatus.tsx"],"names":[],"mappings":"AAQA,qGAAqG;AACrG,QAAA,MAAM,wBAAwB,sDA+B7B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
1
+ {"version":3,"file":"DeskBackgroundSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskBackgroundSyncStatus.tsx"],"names":[],"mappings":"AASA,qGAAqG;AACrG,QAAA,MAAM,wBAAwB,GAAI,eAAe;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,mDA+BtE,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import clsx from "clsx";
3
4
  import { useTranslation } from "react-i18next";
4
5
  import BootProgressBar from "../../components/boot/BootProgressBar";
5
6
  import useBootStore, { CONVERSATIONS_SYNCED_PROGRESS, selectIsSyncingInBackground, } from "../../store/boot";
6
7
  /** Under the list header while a fresh device's first sync keeps pulling the other conversations. */
7
- const DeskBackgroundSyncStatus = () => {
8
+ const DeskBackgroundSyncStatus = ({ className }) => {
8
9
  const { t } = useTranslation("boot");
9
10
  const isSyncing = useBootStore(selectIsSyncingInBackground);
10
11
  const progress = useBootStore((state) => state.syncProgress);
@@ -12,6 +13,6 @@ const DeskBackgroundSyncStatus = () => {
12
13
  return null;
13
14
  // Core reports exactly 10 when the conversations are stored; only later values say much.
14
15
  const showPercent = progress > CONVERSATIONS_SYNCED_PROGRESS;
15
- return (_jsxs("div", { "data-cp-name": "DeskBackgroundSyncStatus", className: "flex flex-col gap-1.5 border-b border-gray-25 px-4 py-2", children: [_jsxs("p", { role: "status", className: "m-0 flex items-center gap-2 text-xs text-gray-600", children: [_jsx("span", { "aria-hidden": "true", className: "h-3 w-3 shrink-0 rounded-full border-2 border-primary-25 border-t-primary-400 motion-safe:animate-spin" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: t("desk.background_sync") }), showPercent && (_jsxs("span", { className: "font-semibold tabular-nums text-primary-400", children: [progress, "%"] }))] }), _jsx(BootProgressBar, { mode: showPercent ? "progress" : "indeterminate", value: progress, label: t("sync_progress_label") })] }));
16
+ return (_jsxs("div", { "data-cp-name": "DeskBackgroundSyncStatus", className: clsx("flex flex-col gap-1.5 border-b border-gray-25 px-4 py-2", className), children: [_jsxs("p", { role: "status", className: "m-0 flex items-center gap-2 text-xs text-gray-600", children: [_jsx("span", { "aria-hidden": "true", className: "h-3 w-3 shrink-0 rounded-full border-2 border-primary-25 border-t-primary-400 motion-safe:animate-spin" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: t("desk.background_sync") }), showPercent && (_jsxs("span", { className: "font-semibold tabular-nums text-primary-400", children: [progress, "%"] }))] }), _jsx(BootProgressBar, { mode: showPercent ? "progress" : "indeterminate", value: progress, label: t("sync_progress_label") })] }));
16
17
  };
17
18
  export default DeskBackgroundSyncStatus;
@@ -11,7 +11,9 @@ export interface ThreadCardProps {
11
11
  muteIndicator?: ReactNode;
12
12
  mentionIndicator?: ReactNode;
13
13
  pinIndicator?: ReactNode;
14
+ /** Draws placeholder bars for the preview and time while the host still loads them. */
15
+ isPreviewLoading?: boolean;
14
16
  }
15
- declare const ThreadCard: ({ avatar, title, preview, time, unreadCount, isSelected, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, }: ThreadCardProps) => import("react/jsx-runtime").JSX.Element;
17
+ declare const ThreadCard: ({ avatar, title, preview, time, unreadCount, isSelected, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, isPreviewLoading, }: ThreadCardProps) => import("react/jsx-runtime").JSX.Element;
16
18
  export default ThreadCard;
17
19
  //# sourceMappingURL=ThreadCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadCard.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ThreadCard.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqBvC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC;CAC1B;AAED,QAAA,MAAM,UAAU,GAAI,4HAYjB,eAAe,4CAgDjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ThreadCard.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ThreadCard.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAqBvC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,gBAAgB,CAAC,EAAE,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,QAAA,MAAM,UAAU,GAAI,8IAajB,eAAe,4CA8DjB,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -7,10 +7,10 @@ const UnreadBadge = ({ count }) => {
7
7
  const isWide = label.length > 1;
8
8
  return (_jsx("span", { className: clsx("inline-flex h-5 shrink-0 items-center justify-center rounded-[32px] border-2 border-white bg-gradient-danger-light px-[3px] text-center text-[12px] font-semibold leading-[160%] text-white", isWide ? "min-w-8" : "min-w-5"), "data-cp-name": "UnreadBadge", children: label }));
9
9
  };
10
- const ThreadCard = ({ avatar, title, preview, time, unreadCount = 0, isSelected = false, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, }) => {
10
+ const ThreadCard = ({ avatar, title, preview, time, unreadCount = 0, isSelected = false, onClick, testId, muteIndicator, mentionIndicator, pinIndicator, isPreviewLoading = false, }) => {
11
11
  const hasUnread = unreadCount > 0;
12
- return (_jsxs("button", { type: "button", onClick: onClick, "data-testid": testId, "data-cp-name": "ThreadCard", className: clsx("relative flex w-full shrink-0 items-start gap-2 border-b border-gray-25 p-3 text-left transition-colors hover:bg-[#f5f5f5] focus:outline-none focus-visible:ring-2 focus-visible:ring-chat-ring", isSelected ? "bg-blue-50" : "bg-white"), children: [isSelected && (_jsx("span", { className: "absolute bottom-0 left-0 top-0 w-1 bg-blue-500" })), avatar, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex min-h-[25.6px] items-center justify-between gap-1", children: [_jsx("div", { className: "min-w-0 flex-1", children: title }), muteIndicator, time && (_jsx("span", { className: "shrink-0 text-[12px] font-normal leading-[160%] text-gray-400 [font-feature-settings:'liga'_off,'clig'_off]", children: time }))] }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: clsx("min-w-0 flex-1 truncate text-[14px] leading-[160%] [font-feature-settings:'liga'_off,'clig'_off]", hasUnread
12
+ return (_jsxs("button", { type: "button", onClick: onClick, "data-testid": testId, "data-cp-name": "ThreadCard", className: clsx("relative flex w-full shrink-0 items-start gap-2 border-b border-gray-25 p-3 text-left transition-colors hover:bg-[#f5f5f5] focus:outline-none focus-visible:ring-2 focus-visible:ring-chat-ring", isSelected ? "bg-blue-50" : "bg-white"), children: [isSelected && (_jsx("span", { className: "absolute bottom-0 left-0 top-0 w-1 bg-blue-500" })), avatar, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex min-h-[25.6px] items-center justify-between gap-1", children: [_jsx("div", { className: "min-w-0 flex-1", children: title }), muteIndicator, time ? (_jsx("span", { className: "shrink-0 text-[12px] font-normal leading-[160%] text-gray-400 [font-feature-settings:'liga'_off,'clig'_off]", children: time })) : (isPreviewLoading && (_jsx("span", { "aria-hidden": "true", className: "h-3 w-9 shrink-0 rounded bg-gray-25 motion-safe:animate-pulse" })))] }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [isPreviewLoading && !preview ? (_jsx("span", { "aria-hidden": "true", "data-cp-name": "ThreadCardPreviewSkeleton", className: "flex h-[22.4px] min-w-0 flex-1 items-center", children: _jsx("span", { className: "h-3 w-4/5 rounded bg-gray-25 motion-safe:animate-pulse" }) })) : (_jsx("p", { className: clsx("min-w-0 flex-1 truncate text-[14px] leading-[160%] [font-feature-settings:'liga'_off,'clig'_off]", hasUnread
13
13
  ? "font-[510] text-black"
14
- : "font-normal tracking-[0.14px] text-gray-600"), children: preview }), mentionIndicator, pinIndicator, hasUnread && _jsx(UnreadBadge, { count: unreadCount })] })] })] }));
14
+ : "font-normal tracking-[0.14px] text-gray-600"), children: preview })), mentionIndicator, pinIndicator, hasUnread && _jsx(UnreadBadge, { count: unreadCount })] })] })] }));
15
15
  };
16
16
  export default ThreadCard;
@@ -0,0 +1,4 @@
1
+ /** A thread opened while the chat engine starts: name the boot step instead of a bare spinner. */
2
+ declare const ConversationDetailBootStatus: () => import("react/jsx-runtime").JSX.Element;
3
+ export default ConversationDetailBootStatus;
4
+ //# sourceMappingURL=ConversationDetailBootStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationDetailBootStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailBootStatus.tsx"],"names":[],"mappings":"AAQA,kGAAkG;AAClG,QAAA,MAAM,4BAA4B,+CAKjC,CAAC;AAEF,eAAe,4BAA4B,CAAC"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Spin } from "antd";
4
+ import InboxBootStatus from "../../components/conversation-inbox/InboxBootStatus";
5
+ import { useChatBootPhase } from "../../hooks/init/useChatBootPhase";
6
+ const DETAIL_SLOW_AFTER_MS = 8000;
7
+ /** A thread opened while the chat engine starts: name the boot step instead of a bare spinner. */
8
+ const ConversationDetailBootStatus = () => {
9
+ const boot = useChatBootPhase({ slowAfterMs: DETAIL_SLOW_AFTER_MS });
10
+ // Booted already (a later reconnect sync): only the thread is loading.
11
+ if (boot.phase === "ready")
12
+ return _jsx(Spin, {});
13
+ return _jsx(InboxBootStatus, { boot: boot });
14
+ };
15
+ export default ConversationDetailBootStatus;
@@ -7,7 +7,8 @@ interface MediaActionsProps {
7
7
  crmComposer?: boolean;
8
8
  hideEmojiOnPhone?: boolean;
9
9
  acceptVideo?: boolean;
10
+ singleDocument?: boolean;
10
11
  }
11
- export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload, crmComposer, hideEmojiOnPhone, acceptVideo, }: MediaActionsProps): import("react/jsx-runtime").JSX.Element;
12
+ export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload, crmComposer, hideEmojiOnPhone, acceptVideo, singleDocument, }: MediaActionsProps): import("react/jsx-runtime").JSX.Element;
12
13
  export {};
13
14
  //# sourceMappingURL=MediaActions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MediaActions.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaActions.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AA8B1C,UAAU,iBAAiB;IACzB,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAsB,EACtB,WAAmB,EACnB,gBAAwB,EACxB,WAAkB,GACnB,EAAE,iBAAiB,2CA4HnB"}
1
+ {"version":3,"file":"MediaActions.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaActions.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAU,UAAU,EAAE,MAAM,MAAM,CAAC;AA8B1C,UAAU,iBAAiB;IACzB,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAsB,EACtB,WAAmB,EACnB,gBAAwB,EACxB,WAAkB,EAClB,cAAsB,GACvB,EAAE,iBAAiB,2CA6HnB"}
@@ -15,7 +15,7 @@ import { useMessageFooterContext } from ".";
15
15
  import useAuthStore from "../../../store/auth";
16
16
  import { StrokeIcon } from "../../../components/icon/StrokeIcon";
17
17
  const EmojiIcon = (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M7.59997 12.4C7.59997 12.4 8.49997 13.2 9.99997 13.2C11.5 13.2 12.4 12.4 12.4 12.4M14 8.39997C14 8.8418 13.6418 9.19997 13.2 9.19997C12.7581 9.19997 12.4 8.8418 12.4 8.39997C12.4 7.95814 12.7581 7.59997 13.2 7.59997C13.6418 7.59997 14 7.95814 14 8.39997ZM18 9.99997C18 14.4182 14.4182 18 9.99997 18C5.58169 18 1.99997 14.4182 1.99997 9.99997C1.99997 5.58169 5.58169 1.99997 9.99997 1.99997C14.4182 1.99997 18 5.58169 18 9.99997ZM7.59997 8.39997C7.59997 8.8418 7.2418 9.19997 6.79997 9.19997C6.35814 9.19997 5.99997 8.8418 5.99997 8.39997C5.99997 7.95814 6.35814 7.59997 6.79997 7.59997C7.2418 7.59997 7.59997 7.95814 7.59997 8.39997Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
18
- export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload = true, crmComposer = false, hideEmojiOnPhone = false, acceptVideo = true, }) {
18
+ export default function MediaActions({ listUploadFiles, onFilesChange, buttonClassName, showMediaUpload = true, crmComposer = false, hideEmojiOnPhone = false, acceptVideo = true, singleDocument = false, }) {
19
19
  const [editor] = useLexicalComposerContext();
20
20
  const { t } = useTranslation();
21
21
  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@@ -72,5 +72,5 @@ export default function MediaActions({ listUploadFiles, onFilesChange, buttonCla
72
72
  return (_jsxs("div", { "data-cp-name": "MediaActions", className: clsx("flex items-center relative", crmComposer ? "gap-0.5" : "gap-1"), ref: containerRef, children: [isCrm && (_jsx(Button, Object.assign({ type: "text", shape: "default", className: clsx(btnClass, isOpenCanned && activeCls), onClick: onPressCanned }, crmLabel("composer_quick_reply"), { children: crmComposer ? (_jsx(StrokeIcon, { name: "hash", size: 18 })) : (_jsx(CannedResponseIcon, { className: clsx("hover:text-gray-700", isOpenCanned && "text-blue-600") })) }))), _jsx(Button, Object.assign({ type: "text", shape: "default", className: clsx(btnClass, hideEmojiOnPhone && "phone:!hidden", showEmojiPicker &&
73
73
  (crmComposer
74
74
  ? activeCls
75
- : "bg-[var(--dc-color-primary-light)] text-[var(--dc-color-primary)]")), onClick: () => setShowEmojiPicker(!showEmojiPicker) }, crmLabel("composer_emoji"), { children: crmComposer ? _jsx(StrokeIcon, { name: "smile", size: 18 }) : EmojiIcon })), showMediaUpload && (_jsx(MediaUploadButtons, { listUploadFiles: listUploadFiles, onFilesChange: onFilesChange, btnClass: btnClass, crmComposer: crmComposer, crmLabel: crmLabel, acceptVideo: acceptVideo })), showEmojiPicker && (_jsx(EmojiPicker, { className: crmComposer ? "max-lg:w-80" : undefined, onEmojiSelect: handleEmojiSelect, onClose: () => setShowEmojiPicker(false) }))] }));
75
+ : "bg-[var(--dc-color-primary-light)] text-[var(--dc-color-primary)]")), onClick: () => setShowEmojiPicker(!showEmojiPicker) }, crmLabel("composer_emoji"), { children: crmComposer ? _jsx(StrokeIcon, { name: "smile", size: 18 }) : EmojiIcon })), showMediaUpload && (_jsx(MediaUploadButtons, { listUploadFiles: listUploadFiles, onFilesChange: onFilesChange, btnClass: btnClass, crmComposer: crmComposer, crmLabel: crmLabel, acceptVideo: acceptVideo, singleDocument: singleDocument })), showEmojiPicker && (_jsx(EmojiPicker, { className: crmComposer ? "max-lg:w-80" : undefined, onEmojiSelect: handleEmojiSelect, onClose: () => setShowEmojiPicker(false) }))] }));
76
76
  }
@@ -6,7 +6,8 @@ interface MediaUploadButtonsProps {
6
6
  crmComposer: boolean;
7
7
  crmLabel: (key: string) => Record<string, string | undefined>;
8
8
  acceptVideo?: boolean;
9
+ singleDocument?: boolean;
9
10
  }
10
- export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btnClass, crmComposer, crmLabel, acceptVideo, }: MediaUploadButtonsProps): import("react/jsx-runtime").JSX.Element;
11
+ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btnClass, crmComposer, crmLabel, acceptVideo, singleDocument, }: MediaUploadButtonsProps): import("react/jsx-runtime").JSX.Element;
11
12
  export {};
12
13
  //# sourceMappingURL=MediaUploadButtons.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MediaUploadButtons.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaUploadButtons.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,UAAU,EAA0B,MAAM,MAAM,CAAC;AA2B1E,UAAU,uBAAuB;IAC/B,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,WAAkB,GACnB,EAAE,uBAAuB,2CA6EzB"}
1
+ {"version":3,"file":"MediaUploadButtons.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaUploadButtons.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,UAAU,EAA0B,MAAM,MAAM,CAAC;AA2B1E,UAAU,uBAAuB;IAC/B,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9D,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,WAAkB,EAClB,cAAsB,GACvB,EAAE,uBAAuB,2CA6FzB"}
@@ -3,12 +3,14 @@ import { Button, Upload, message as antdMessage } from "antd";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { Icon } from "../../../components/icon";
5
5
  import { StrokeIcon } from "../../../components/icon/StrokeIcon";
6
- import { validateFile } from "../../../utils/fileValidation";
6
+ import { MAX_DOCUMENT_SIZE_MB, validateFile } from "../../../utils/fileValidation";
7
7
  import { DOCUMENT_TYPES } from "./editorConfig";
8
8
  import { mergePickedUploadFiles } from "./mergePickedUploadFiles";
9
9
  const AttachIcon = (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M15.6 10.0001V11.2001C15.6 14.5137 12.9137 17.2001 9.59998 17.2001C6.28626 17.2001 3.59998 14.5137 3.59998 11.2001V6.79999C3.59998 4.59085 5.39084 2.79999 7.59998 2.79999C9.8091 2.79999 11.6 4.59085 11.6 6.79999V11.2C11.6 12.3045 10.7045 13.2 9.59998 13.2C8.49542 13.2 7.59998 12.3045 7.59998 11.2V7.99999", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
10
- export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btnClass, crmComposer, crmLabel, acceptVideo = true, }) {
10
+ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btnClass, crmComposer, crmLabel, acceptVideo = true, singleDocument = false, }) {
11
11
  const { t } = useTranslation();
12
+ const documentLocked = singleDocument &&
13
+ listUploadFiles.some((f) => DOCUMENT_TYPES.includes(f.type || ""));
12
14
  const beforeUploadImagesAndVideo = (file) => {
13
15
  if (!acceptVideo && file.type.startsWith("video/")) {
14
16
  antdMessage.error(t("invalid_image_format", { fileName: file.name }));
@@ -22,11 +24,21 @@ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btn
22
24
  return false;
23
25
  };
24
26
  const beforeUploadFile = (file) => {
27
+ // The disabled button stops clicks; this also stops files set on the input directly.
28
+ if (documentLocked)
29
+ return Upload.LIST_IGNORE;
25
30
  const isAllowed = DOCUMENT_TYPES.includes(file.type);
26
31
  if (!isAllowed) {
27
32
  antdMessage.error(t("invalid_document_format", { fileName: file.name }));
28
33
  return Upload.LIST_IGNORE;
29
34
  }
35
+ if (file.size / 1024 / 1024 > MAX_DOCUMENT_SIZE_MB) {
36
+ antdMessage.error(t("file_size_exceeded", {
37
+ fileName: file.name,
38
+ maxSize: MAX_DOCUMENT_SIZE_MB,
39
+ }));
40
+ return Upload.LIST_IGNORE;
41
+ }
30
42
  return false;
31
43
  };
32
44
  const handleChange = (info) => {
@@ -34,5 +46,5 @@ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btn
34
46
  };
35
47
  return (_jsxs(_Fragment, { children: [_jsx(Upload, { accept: acceptVideo
36
48
  ? "image/jpeg, image/png, image/jpg, video/*"
37
- : "image/jpeg, image/png, image/jpg", beforeUpload: beforeUploadImagesAndVideo, multiple: true, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass }, crmLabel("composer_attach_image"), { children: crmComposer ? (_jsx(StrokeIcon, { name: "image", size: 18 })) : (_jsx(Icon, { icon: "image-02-o", size: 22 })) })) }), _jsx(Upload, { accept: ".doc,.docx,.pdf", beforeUpload: beforeUploadFile, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass }, crmLabel("composer_attach_file"), { children: crmComposer ? _jsx(StrokeIcon, { name: "paperclip", size: 18 }) : AttachIcon })) })] }));
49
+ : "image/jpeg, image/png, image/jpg", beforeUpload: beforeUploadImagesAndVideo, multiple: true, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass }, crmLabel("composer_attach_image"), { children: crmComposer ? (_jsx(StrokeIcon, { name: "image", size: 18 })) : (_jsx(Icon, { icon: "image-02-o", size: 22 })) })) }), _jsx(Upload, { accept: ".doc,.docx,.pdf", beforeUpload: beforeUploadFile, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, disabled: documentLocked, children: _jsx(Button, Object.assign({ type: "text", shape: "default", className: btnClass, disabled: documentLocked }, crmLabel("composer_attach_file"), { children: crmComposer ? _jsx(StrokeIcon, { name: "paperclip", size: 18 }) : AttachIcon })) })] }));
38
50
  }
@@ -15,7 +15,9 @@ interface RichTextEditorProps {
15
15
  files?: UploadFile[];
16
16
  /** false: the image picker rejects video (default true). */
17
17
  acceptVideo?: boolean;
18
+ /** true: one document max; attach-file is disabled while one is attached. */
19
+ singleDocument?: boolean;
18
20
  }
19
- export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight, showMediaUpload, hideEmojiOnPhone, files, acceptVideo, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
21
+ export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight, showMediaUpload, hideEmojiOnPhone, files, acceptVideo, singleDocument, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
20
22
  export {};
21
23
  //# sourceMappingURL=RichTextEditor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../../src/components/rich-text-editor/RichTextEditor.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA2ClC,UAAU,mBAAmB;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EACrC,WAAW,EACX,WAAW,EACX,QAAQ,EACR,aAAa,EACb,SAAmB,EACnB,eAAsB,EACtB,gBAAwB,EACxB,KAAK,EACL,WAAkB,GACnB,EAAE,mBAAmB,2CAkErB"}
1
+ {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../../src/components/rich-text-editor/RichTextEditor.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA2ClC,UAAU,mBAAmB;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EACrC,WAAW,EACX,WAAW,EACX,QAAQ,EACR,aAAa,EACb,SAAmB,EACnB,eAAsB,EACtB,gBAAwB,EACxB,KAAK,EACL,WAAkB,EAClB,cAAsB,GACvB,EAAE,mBAAmB,2CAmErB"}
@@ -39,7 +39,7 @@ function HtmlPlugin({ initialHtml }) {
39
39
  }, [editor, initialHtml]);
40
40
  return null;
41
41
  }
42
- export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight = "160px", showMediaUpload = true, hideEmojiOnPhone = false, files, acceptVideo = true, }) {
42
+ export default function RichTextEditor({ initialHtml, placeholder, onChange, onFilesChange, minHeight = "160px", showMediaUpload = true, hideEmojiOnPhone = false, files, acceptVideo = true, singleDocument = false, }) {
43
43
  const [innerFiles, setInnerFiles] = useState([]);
44
44
  const listUploadFiles = files !== null && files !== void 0 ? files : innerFiles;
45
45
  const handleChange = (editorState, editor, tags) => {
@@ -60,5 +60,5 @@ export default function RichTextEditor({ initialHtml, placeholder, onChange, onF
60
60
  onFilesChange === null || onFilesChange === void 0 ? void 0 : onFilesChange(next);
61
61
  };
62
62
  return (_jsxs(LexicalComposer, { initialConfig: EDITOR_CONFIG, children: [_jsxs("div", { className: "border border-gray-200 rounded-lg", children: [_jsx(FilePreviewList, { files: listUploadFiles, onRemove: handleRemoveFile }), _jsx("div", { className: "relative", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "px-3 py-2 text-sm outline-none", style: { minHeight } }), ErrorBoundary: LexicalErrorBoundary, placeholder: placeholder ? (_jsx("div", { className: "absolute top-2 left-3 pointer-events-none", children: _jsx("p", { className: "text-gray-400 text-sm", children: placeholder }) })) : null }) }), _jsxs("div", { className: clsx("flex items-center border-t border-gray-100 px-2 py-1", hideEmojiOnPhone &&
63
- "flex-wrap max-sm:[&_[data-cp-name=ToolbarPlugin]]:!gap-1 max-sm:[&_[data-cp-name=ToolbarPlugin]]:!px-1"), children: [_jsx(ToolbarPlugin, {}), _jsx(MediaActions, { listUploadFiles: listUploadFiles, onFilesChange: handleFilesChange, showMediaUpload: showMediaUpload, hideEmojiOnPhone: hideEmojiOnPhone, acceptVideo: acceptVideo })] })] }), _jsx(LinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(HtmlPlugin, { initialHtml: initialHtml })] }));
63
+ "flex-wrap max-sm:[&_[data-cp-name=ToolbarPlugin]]:!gap-1 max-sm:[&_[data-cp-name=ToolbarPlugin]]:!px-1"), children: [_jsx(ToolbarPlugin, {}), _jsx(MediaActions, { listUploadFiles: listUploadFiles, onFilesChange: handleFilesChange, showMediaUpload: showMediaUpload, hideEmojiOnPhone: hideEmojiOnPhone, acceptVideo: acceptVideo, singleDocument: singleDocument })] })] }), _jsx(LinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(HtmlPlugin, { initialHtml: initialHtml })] }));
64
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SessionDetailCard.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionDetailCard.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAsD/C,eAAO,MAAM,iBAAiB,GAAI,wBAG/B;IACD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB,4CAqMA,CAAC"}
1
+ {"version":3,"file":"SessionDetailCard.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionDetailCard.tsx"],"names":[],"mappings":"AAyBA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAsD/C,eAAO,MAAM,iBAAiB,GAAI,wBAG/B;IACD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB,4CAoNA,CAAC"}
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useId, useRef, useState } from "react";
3
- import { Button, Input, message } from "antd";
2
+ import { useCallback, useId, useMemo, useState } from "react";
3
+ import { Button, Input } from "antd";
4
4
  import { StrokeIcon } from "../../components/icon/StrokeIcon";
5
5
  import TintedAvatar from "../../components/common/TintedAvatar";
6
6
  import { useQueryClient } from "@tanstack/react-query";
7
7
  import { useTranslation } from "react-i18next";
8
8
  import ManualAssignPopover from "./ManualAssignPopover";
9
9
  import SessionTagPicker from "./SessionTagPicker";
10
+ import SessionLabelRow, { readOnlyValueClass, sessionFieldClass } from "./SessionLabelRow";
10
11
  import EndSessionConfirm from "./EndSessionConfirm";
11
12
  import { formatSessionDate, formatSessionTime } from "./sessionDateFormat";
12
13
  import { useGetLabelSession } from "../../hooks/session/useGetLabelSession";
13
14
  import { useUpdateSessionInfo } from "../../hooks/session/useUpdateSessionInfo";
14
15
  import { useSessionFieldAutosave, } from "../../hooks/session/useSessionFieldAutosave";
16
+ import { useSessionLabelsAutosave, } from "../../hooks/session/useSessionLabelsAutosave";
15
17
  import { useEndSession } from "../../hooks/session/useEndSession";
16
18
  import { useIsSessionClosePending } from "../../hooks/session/usePendingSessionClose";
17
19
  import { useSessionCustomerName } from "../../hooks/session/useSessionCustomerName";
@@ -37,10 +39,10 @@ const SaveStatus = ({ status, onRetry, }) => {
37
39
  ? "text-success-900"
38
40
  : "text-gray-600"}`, children: [status === "saving" && t("save_status_saving"), status === "saved" && (_jsxs(_Fragment, { children: [_jsx(StrokeIcon, { name: "check", size: 14 }), t("save_status_saved")] })), status === "error" && (_jsxs(_Fragment, { children: [t("save_status_failed"), " \u00B7", " ", _jsx("button", { type: "button", className: "underline", onClick: onRetry, children: t("retry") })] }))] }));
39
41
  };
40
- const fieldLabelClass = "text-xs font-medium text-gray-600";
41
- const fieldClass = "!rounded-lg !border-gray-50 !px-2.5 !py-1.5 !text-sm";
42
+ const fieldLabelClass = "text-[13px] font-semibold text-gray-900";
43
+ const emptyValueClass = "m-0 text-sm text-gray-600";
42
44
  export const SessionDetailCard = ({ session, isActive, }) => {
43
- var _a, _b, _c, _d, _e;
45
+ var _a, _b, _c, _d, _e, _f;
44
46
  const queryClient = useQueryClient();
45
47
  const { t } = useTranslation("common");
46
48
  const ids = useId();
@@ -50,11 +52,6 @@ export const SessionDetailCard = ({ session, isActive, }) => {
50
52
  const isEnding = useIsSessionClosePending(session.id);
51
53
  const customerName = useSessionCustomerName();
52
54
  const [isEndConfirmOpen, setIsEndConfirmOpen] = useState(false);
53
- const [tagStatus, setTagStatus] = useState("idle");
54
- const tagSaveSeq = useRef(0);
55
- // Holds the picked tags until the session list refetch confirms them, so a quick second tick keeps the first.
56
- const [pendingLabelIds, setPendingLabelIds] = useState(null);
57
- const selectedLabelIds = (_b = pendingLabelIds !== null && pendingLabelIds !== void 0 ? pendingLabelIds : (_a = session.labels) === null || _a === void 0 ? void 0 : _a.map((label) => label.id)) !== null && _b !== void 0 ? _b : [];
58
55
  const saveField = useCallback(async (data) => {
59
56
  await updateSession(Object.assign({ sessionId: session.id }, data));
60
57
  queryClient.invalidateQueries({
@@ -65,38 +62,29 @@ export const SessionDetailCard = ({ session, isActive, }) => {
65
62
  serverValue: session.note || "",
66
63
  save: (value) => saveField({ note: value }),
67
64
  });
68
- const issueDetail = useSessionFieldAutosave({
69
- serverValue: session.issueDetail || "",
70
- save: (value) => saveField({ issueDetail: value }),
65
+ const serverLabels = useMemo(() => { var _a; return ((_a = session.labels) !== null && _a !== void 0 ? _a : []).map(({ id, note }) => ({ id, note: note !== null && note !== void 0 ? note : "" })); }, [session.labels]);
66
+ const labels = useSessionLabelsAutosave({
67
+ serverLabels,
68
+ save: (next) => saveField({
69
+ labels: next.map(({ id, note }) => ({ id, note: note || null })),
70
+ // A chat-service without per-label notes still reads tags from labelIds; newer ones use labels.
71
+ labelIds: next.map(({ id }) => id),
72
+ }),
71
73
  });
72
- const saveStatus = mergeAutosaveStatus(note.status, issueDetail.status, tagStatus);
74
+ const labelNames = new Map([...(availableLabels !== null && availableLabels !== void 0 ? availableLabels : []), ...((_a = session.labels) !== null && _a !== void 0 ? _a : [])].map((label) => [
75
+ label.id,
76
+ label.name,
77
+ ]));
78
+ const selectedLabelIds = labels.labels.map((label) => label.id);
79
+ // A closed session keeps its tags and notes as they were saved.
80
+ const readOnly = !isActive;
81
+ const fieldLabel = (htmlFor, text) => readOnly ? (_jsx("span", { className: fieldLabelClass, children: text })) : (_jsx("label", { htmlFor: htmlFor, className: fieldLabelClass, children: text }));
82
+ const saveStatus = mergeAutosaveStatus(note.status, labels.status);
73
83
  const retrySave = () => {
74
84
  if (note.status === "error")
75
85
  void note.retry();
76
- if (issueDetail.status === "error")
77
- void issueDetail.retry();
78
- };
79
- const handleUpdateLabels = (labelIds) => {
80
- const seq = ++tagSaveSeq.current;
81
- setPendingLabelIds(labelIds);
82
- setTagStatus("saving");
83
- updateSession({ sessionId: session.id, labelIds })
84
- .then(async () => {
85
- if (seq === tagSaveSeq.current)
86
- setTagStatus("saved");
87
- await queryClient.invalidateQueries({
88
- queryKey: [QUERY_KEYS.GET_LIST_SESSION_BY_CONVERSATION],
89
- });
90
- if (seq === tagSaveSeq.current)
91
- setPendingLabelIds(null);
92
- })
93
- .catch(() => {
94
- if (seq === tagSaveSeq.current) {
95
- setTagStatus("idle");
96
- setPendingLabelIds(null);
97
- }
98
- message.error(t("update_session_failed"));
99
- });
86
+ if (labels.status === "error")
87
+ void labels.retry();
100
88
  };
101
89
  const handleConfirmEnd = () => {
102
90
  setIsEndConfirmOpen(false);
@@ -106,6 +94,9 @@ export const SessionDetailCard = ({ session, isActive, }) => {
106
94
  ? t("session_started_at", {
107
95
  time: formatSessionTime(session.createdDate),
108
96
  })
109
- : formatSessionDate(session.closedDate || session.createdDate) }), _jsx("span", { className: "flex-1" }), isActive ? (_jsx("span", { className: "inline-flex h-[22px] items-center rounded-full bg-success-25 px-2 text-xs font-medium text-success-900", children: t("session_active") })) : (_jsx(TintedAvatar, { name: ((_c = session.supporter) === null || _c === void 0 ? void 0 : _c.fullName) || "A", src: (_d = session.supporter) === null || _d === void 0 ? void 0 : _d.avatar, size: 24 }))] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: `${ids}-tags`, className: fieldLabelClass, children: t("session_tags_label") }), _jsx(SessionTagPicker, { labelId: `${ids}-tags`, available: availableLabels !== null && availableLabels !== void 0 ? availableLabels : [], selectedIds: selectedLabelIds, onChange: handleUpdateLabels })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { htmlFor: `${ids}-issue`, className: fieldLabelClass, children: t("session_issue_detail_label") }), _jsx(TextArea, { id: `${ids}-issue`, value: issueDetail.value, onChange: (e) => issueDetail.setValue(e.target.value), placeholder: t("session_issue_detail_placeholder"), className: `${fieldClass} !min-h-9`, autoSize: { minRows: 1, maxRows: 3 } })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { htmlFor: `${ids}-note`, className: fieldLabelClass, children: t("session_note_label") }), _jsx(SaveStatus, { status: saveStatus, onRetry: retrySave })] }), _jsx(TextArea, { id: `${ids}-note`, value: note.value, onChange: (e) => note.setValue(e.target.value), placeholder: t("session_note_placeholder"), className: fieldClass, autoSize: { minRows: 2, maxRows: 4 } })] }), isActive && (_jsxs(_Fragment, { children: [session.status !== SessionStatus.UNASSIGNED &&
110
- ((_e = session.supporter) === null || _e === void 0 ? void 0 : _e.id) && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TintedAvatar, { name: session.supporter.fullName || "A", src: session.supporter.avatar, size: 28 }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "text-xs text-gray-600", children: t("session_owner_label") }), _jsx("span", { className: "truncate text-[13px] font-medium text-gray-900", children: session.supporter.fullName })] }), _jsx(ManualAssignPopover, { sessionId: session.id, supporter: session.supporter, teamId: session.teamId, placement: "topRight", children: _jsx(Button, { icon: _jsx(StrokeIcon, { name: "shuffle", size: 18 }), className: "!h-8 touch:!h-10 !rounded-lg !px-3.5 !text-[13px] !font-medium", children: t("manual_assign_button_reassign") }) })] })), _jsx(Button, { block: true, icon: _jsx(StrokeIcon, { name: "check-circle", size: 18 }), onClick: () => setIsEndConfirmOpen(true), disabled: isEnding, children: isEnding ? t("end_session_ending") : t("session_close_button") }), _jsx(EndSessionConfirm, { open: isEndConfirmOpen, session: session, customerName: customerName, onConfirm: handleConfirmEnd, onCancel: () => setIsEndConfirmOpen(false) })] }))] }));
97
+ : formatSessionDate(session.closedDate || session.createdDate) }), _jsx("span", { className: "flex-1" }), isActive ? (_jsx("span", { className: "inline-flex h-[22px] items-center rounded-full bg-success-25 px-2 text-xs font-medium text-success-900", children: t("session_active") })) : (_jsx(TintedAvatar, { name: ((_b = session.supporter) === null || _b === void 0 ? void 0 : _b.fullName) || "A", src: (_c = session.supporter) === null || _c === void 0 ? void 0 : _c.avatar, size: 24 }))] }), _jsxs("div", { className: "flex flex-col gap-1", children: [fieldLabel(`${ids}-tags`, t("session_tags_label")), readOnly && labels.labels.length === 0 ? (_jsx("p", { className: emptyValueClass, children: "\u2014" })) : (_jsxs("div", { className: "divide-y divide-gray-50 rounded-lg border border-gray-50", children: [!readOnly && (_jsx(SessionTagPicker, { labelId: `${ids}-tags`, available: availableLabels !== null && availableLabels !== void 0 ? availableLabels : [], selectedIds: selectedLabelIds, onChange: labels.setLabelIds })), labels.labels.map((label) => {
98
+ var _a;
99
+ return (_jsx(SessionLabelRow, { name: (_a = labelNames.get(label.id)) !== null && _a !== void 0 ? _a : "", note: label.note, readOnly: readOnly, onNoteChange: (value) => labels.setNote(label.id, value), onRemove: () => labels.setLabelIds(selectedLabelIds.filter((id) => id !== label.id)) }, label.id));
100
+ })] }))] }), session.issueDetail && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: fieldLabelClass, children: t("session_issue_detail_label") }), _jsx("p", { className: readOnlyValueClass, "data-testid": "session-legacy-issue", children: session.issueDetail })] })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [fieldLabel(`${ids}-note`, t("session_note_label")), !readOnly && _jsx(SaveStatus, { status: saveStatus, onRetry: retrySave })] }), readOnly ? (_jsx("p", { className: ((_d = session.note) === null || _d === void 0 ? void 0 : _d.trim()) ? readOnlyValueClass : emptyValueClass, "data-testid": "session-note-text", children: ((_e = session.note) === null || _e === void 0 ? void 0 : _e.trim()) ? session.note : "—" })) : (_jsx(TextArea, { id: `${ids}-note`, value: note.value, onChange: (e) => note.setValue(e.target.value), placeholder: t("session_note_placeholder"), className: sessionFieldClass, autoSize: { minRows: 2, maxRows: 4 } }))] }), isActive && (_jsxs(_Fragment, { children: [session.status !== SessionStatus.UNASSIGNED &&
101
+ ((_f = session.supporter) === null || _f === void 0 ? void 0 : _f.id) && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TintedAvatar, { name: session.supporter.fullName || "A", src: session.supporter.avatar, size: 28 }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "text-xs text-gray-600", children: t("session_owner_label") }), _jsx("span", { className: "truncate text-[13px] font-medium text-gray-900", children: session.supporter.fullName })] }), _jsx(ManualAssignPopover, { sessionId: session.id, supporter: session.supporter, teamId: session.teamId, placement: "topRight", children: _jsx(Button, { icon: _jsx(StrokeIcon, { name: "shuffle", size: 18 }), className: "!h-8 touch:!h-10 !rounded-lg !px-3.5 !text-[13px] !font-medium", children: t("manual_assign_button_reassign") }) })] })), _jsx(Button, { block: true, icon: _jsx(StrokeIcon, { name: "check-circle", size: 18 }), onClick: () => setIsEndConfirmOpen(true), disabled: isEnding, children: isEnding ? t("end_session_ending") : t("session_close_button") }), _jsx(EndSessionConfirm, { open: isEndConfirmOpen, session: session, customerName: customerName, onConfirm: handleConfirmEnd, onCancel: () => setIsEndConfirmOpen(false) })] }))] }));
111
102
  };
@@ -0,0 +1,15 @@
1
+ /** Server limit for one label's note. */
2
+ export declare const SESSION_LABEL_NOTE_MAX_LENGTH = 1024;
3
+ export declare const sessionFieldClass = "!rounded-lg !border-gray-50 !px-2.5 !py-1.5 !text-sm";
4
+ export declare const readOnlyValueClass = "m-0 whitespace-pre-wrap break-words text-sm text-gray-900";
5
+ interface SessionLabelRowProps {
6
+ name: string;
7
+ note: string;
8
+ /** Closed session: no remove button, and the note as plain text. */
9
+ readOnly?: boolean;
10
+ onNoteChange: (note: string) => void;
11
+ onRemove: () => void;
12
+ }
13
+ declare const SessionLabelRow: ({ name, note, readOnly, onNoteChange, onRemove, }: SessionLabelRowProps) => import("react/jsx-runtime").JSX.Element;
14
+ export default SessionLabelRow;
15
+ //# sourceMappingURL=SessionLabelRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionLabelRow.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionLabelRow.tsx"],"names":[],"mappings":"AAQA,yCAAyC;AACzC,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD,eAAO,MAAM,iBAAiB,yDAC0B,CAAC;AAEzD,eAAO,MAAM,kBAAkB,8DAA8D,CAAC;AAE9F,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAGD,QAAA,MAAM,eAAe,GAAI,mDAMtB,oBAAoB,4CAqFtB,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { Input } from "antd";
4
+ import { useTranslation } from "react-i18next";
5
+ import { StrokeIcon } from "../../components/icon/StrokeIcon";
6
+ const { TextArea } = Input;
7
+ /** Server limit for one label's note. */
8
+ export const SESSION_LABEL_NOTE_MAX_LENGTH = 1024;
9
+ export const sessionFieldClass = "!rounded-lg !border-gray-50 !px-2.5 !py-1.5 !text-sm";
10
+ export const readOnlyValueClass = "m-0 whitespace-pre-wrap break-words text-sm text-gray-900";
11
+ // One picked label: its chip, then its own "Vấn đề cụ thể" note.
12
+ const SessionLabelRow = ({ name, note, readOnly = false, onNoteChange, onRemove, }) => {
13
+ const { t } = useTranslation("common");
14
+ const [isEditing, setIsEditing] = useState(false);
15
+ const inputRef = useRef(null);
16
+ const pointerDownRef = useRef(false);
17
+ const noteTextId = useId();
18
+ const noteLabel = t("session_label_note_aria", { name });
19
+ useEffect(() => {
20
+ var _a;
21
+ if (!isEditing)
22
+ return;
23
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus({ cursor: "end" });
24
+ const onDown = () => (pointerDownRef.current = true);
25
+ const onUp = () => (pointerDownRef.current = false);
26
+ document.addEventListener("pointerdown", onDown, true);
27
+ document.addEventListener("pointerup", onUp, true);
28
+ document.addEventListener("pointercancel", onUp, true);
29
+ return () => {
30
+ document.removeEventListener("pointerdown", onDown, true);
31
+ document.removeEventListener("pointerup", onUp, true);
32
+ document.removeEventListener("pointercancel", onUp, true);
33
+ pointerDownRef.current = false;
34
+ };
35
+ }, [isEditing]);
36
+ // Collapsing shrinks the row, so let the click that caused the blur land first
37
+ // (mouse: after pointerup; touch fires blur and click in one task). A window or
38
+ // tab switch keeps the editor open to come back to.
39
+ const handleBlur = (event) => {
40
+ const collapse = () => setTimeout(() => setIsEditing(false));
41
+ if (pointerDownRef.current) {
42
+ window.addEventListener("pointerup", collapse, { once: true });
43
+ return;
44
+ }
45
+ if (!event.relatedTarget && !document.hasFocus())
46
+ return;
47
+ collapse();
48
+ };
49
+ return (_jsxs("div", { className: "flex flex-col gap-2 px-3 py-2.5", "data-testid": "session-label-row", children: [_jsxs("span", { className: "inline-flex h-6 max-w-full items-center gap-1 self-start rounded-md bg-[var(--dc-color-neutral-surface)] px-2 text-[13px] font-semibold text-gray-900", children: [_jsx("span", { className: "truncate", children: name }), !readOnly && (_jsx("button", { type: "button", "aria-label": t("session_remove_tag", { name }), className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-gray-600 hover:bg-gray-50", onClick: onRemove, children: _jsx(StrokeIcon, { name: "x", size: 12 }) }))] }), readOnly ? (note.trim() && _jsx("p", { className: readOnlyValueClass, children: note })) : note && !isEditing ? (
50
+ // A filled note shows two lines until it is focused, like the design.
51
+ _jsx("button", { type: "button", "aria-label": noteLabel, "aria-describedby": noteTextId, className: "min-h-9 w-full rounded-lg border border-gray-50 px-2.5 py-1.5 text-left text-sm text-gray-900 hover:border-gray-300", onClick: () => setIsEditing(true), onFocus: () => setIsEditing(true), children: _jsx("span", { id: noteTextId, className: "line-clamp-2 whitespace-pre-wrap break-words", children: note }) })) : (_jsx(TextArea, { ref: inputRef, "aria-label": noteLabel, value: note, maxLength: SESSION_LABEL_NOTE_MAX_LENGTH, onChange: (event) => onNoteChange(event.target.value), onFocus: () => setIsEditing(true), onBlur: handleBlur, placeholder: t("session_issue_detail_placeholder"), className: `${sessionFieldClass} !min-h-9`, autoSize: { minRows: 1, maxRows: 4 } }))] }));
52
+ };
53
+ export default SessionLabelRow;
@@ -4,7 +4,8 @@ export { SessionDetailCard };
4
4
  interface SessionSectionProps {
5
5
  sessions: ISessionResponse[];
6
6
  isLoading: boolean;
7
+ isFetchingMore?: boolean;
7
8
  }
8
- declare const SessionSection: ({ sessions, isLoading }: SessionSectionProps) => import("react/jsx-runtime").JSX.Element;
9
+ declare const SessionSection: ({ sessions, isLoading, isFetchingMore, }: SessionSectionProps) => import("react/jsx-runtime").JSX.Element;
9
10
  export default SessionSection;
10
11
  //# sourceMappingURL=SessionSection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionSection.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionSection.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAoF7B,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,cAAc,GAAI,yBAAyB,mBAAmB,4CAmCnE,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"SessionSection.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionSection.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAoF7B,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,QAAA,MAAM,cAAc,GAAI,0CAIrB,mBAAmB,4CAwCrB,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -28,12 +28,12 @@ const ClosedSessionItem = ({ session, expandedSessionId, onToggleExpand, }) => {
28
28
  .join(" · ");
29
29
  return (_jsxs("button", { type: "button", "aria-expanded": false, className: "flex w-full flex-col gap-1 border-t border-gray-25 py-2.5 text-left hover:bg-gray-5", onClick: () => onToggleExpand(session.id), "data-testid": "past-session-row", children: [_jsxs("span", { className: "flex items-center justify-between gap-2 text-[13px]", children: [_jsx("span", { className: "font-semibold text-gray-900", children: t("session_label", { order: session.order }) }), _jsx("span", { className: "text-gray-600", children: dateText })] }), _jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs text-gray-600", children: [tags.slice(0, MAX_VISIBLE_TAGS).map((label) => (_jsx("span", { className: "inline-flex h-5 max-w-[120px] shrink-0 items-center truncate rounded-full bg-[var(--dc-color-neutral-surface)] px-2 font-medium text-gray-900", children: label.name }, label.id))), tags.length > MAX_VISIBLE_TAGS && (_jsxs("span", { className: "shrink-0", children: ["+", tags.length - MAX_VISIBLE_TAGS] })), _jsx("span", { className: "truncate", children: (_b = session.supporter) === null || _b === void 0 ? void 0 : _b.fullName })] })] }));
30
30
  };
31
- const SessionSection = ({ sessions, isLoading }) => {
31
+ const SessionSection = ({ sessions, isLoading, isFetchingMore = false, }) => {
32
32
  const { t } = useTranslation("common");
33
33
  const [expandedSessionId, setExpandedSessionId] = useState(null);
34
34
  const handleToggleExpand = (sessionId) => {
35
35
  setExpandedSessionId((prevId) => (prevId === sessionId ? null : sessionId));
36
36
  };
37
- return (_jsxs("div", { className: "flex flex-col px-4 pb-2 pt-3", "data-cp-name": "SessionSection", children: [_jsx("h3", { className: "mb-1 text-[13px] font-semibold text-gray-900", children: t("sessions_previous_heading") }), isLoading ? (_jsx("div", { className: "mt-4 text-center", children: _jsx(Spin, {}) })) : sessions.length === 0 ? (_jsx("p", { className: "py-2 text-xs text-gray-600", children: t("sessions_previous_empty") })) : (_jsx("div", { className: "flex flex-col", children: sessions.map((session) => (_jsx(ClosedSessionItem, { session: session, expandedSessionId: expandedSessionId, onToggleExpand: handleToggleExpand }, session.id))) }))] }));
37
+ return (_jsxs("div", { className: "flex flex-col px-4 pb-2 pt-3", "data-cp-name": "SessionSection", children: [_jsx("h3", { className: "mb-1 text-[13px] font-semibold text-gray-900", children: t("sessions_previous_heading") }), isLoading ? (_jsx("div", { className: "mt-4 text-center", children: _jsx(Spin, {}) })) : sessions.length === 0 ? (_jsx("p", { className: "py-2 text-xs text-gray-600", children: t("sessions_previous_empty") })) : (_jsxs("div", { className: "flex flex-col", children: [sessions.map((session) => (_jsx(ClosedSessionItem, { session: session, expandedSessionId: expandedSessionId, onToggleExpand: handleToggleExpand }, session.id))), isFetchingMore && (_jsx("div", { className: "py-2 text-center", "data-testid": "past-sessions-loading", children: _jsx(Spin, { size: "small" }) }))] }))] }));
38
38
  };
39
39
  export default SessionSection;
@@ -1 +1 @@
1
- {"version":3,"file":"SessionTagPicker.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionTagPicker.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,UAAU,qBAAqB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACnC;AASD,QAAA,MAAM,gBAAgB,GAAI,gDAKvB,qBAAqB,4CAmIvB,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"SessionTagPicker.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionTagPicker.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,UAAU,qBAAqB;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACnC;AAMD,QAAA,MAAM,gBAAgB,GAAI,gDAKvB,qBAAqB,4CAkHvB,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -9,10 +9,9 @@ import { useGetLabelSession } from "../../hooks/session/useGetLabelSession";
9
9
  import { useEscapeLayer } from "../../hooks/common/useEscapeLayer";
10
10
  import { useMediaQuery } from "../../hooks/common/useMediaQuery";
11
11
  import { LAYOUT_MEDIA } from "../../constants/layout";
12
- const chipClass = "inline-flex h-[22px] items-center gap-1 whitespace-nowrap rounded-full bg-[var(--dc-color-neutral-surface)] px-2 text-xs font-medium text-gray-900";
13
12
  const OPTION_HEIGHT = 36;
14
13
  const MAX_VISIBLE_OPTIONS = 8;
15
- // The option list opens inline under the field, so it pushes the card instead of floating over it.
14
+ // The "Thêm thẻ" row of the tag box. Its list opens inline, so it pushes the card instead of floating over it.
16
15
  const SessionTagPicker = ({ labelId, available, selectedIds, onChange, }) => {
17
16
  const { t } = useTranslation("common");
18
17
  const isTouch = useMediaQuery(LAYOUT_MEDIA.touch);
@@ -26,8 +25,6 @@ const SessionTagPicker = ({ labelId, available, selectedIds, onChange, }) => {
26
25
  const listId = useId();
27
26
  const toggleRef = useRef(null);
28
27
  const panelRef = useRef(null);
29
- // Chips follow the order the tags were picked (selectedIds), not the label list order.
30
- const selected = selectedIds.flatMap((id) => available.filter((label) => label.id === id));
31
28
  // Sized from the full list, not the results, so searching never resizes it.
32
29
  const listHeight = Math.min(Math.max(available.length, 1), MAX_VISIBLE_OPTIONS) * OPTION_HEIGHT;
33
30
  const close = ({ restoreFocus = true } = {}) => {
@@ -53,7 +50,7 @@ const SessionTagPicker = ({ labelId, available, selectedIds, onChange, }) => {
53
50
  event.stopPropagation();
54
51
  close();
55
52
  }
56
- }, children: [_jsxs("div", { className: "flex min-h-9 flex-wrap items-center gap-1 rounded-lg border border-gray-50 px-1.5 py-1", children: [selected.map((label) => (_jsxs("span", { className: chipClass, children: [label.name, _jsx("button", { type: "button", "aria-label": t("session_remove_tag", { name: label.name }), className: "flex h-4 w-4 items-center justify-center rounded-full text-gray-600 hover:bg-gray-50", onClick: () => toggle(label.id), children: _jsx(StrokeIcon, { name: "x", size: 12 }) })] }, label.id))), _jsxs("button", { ref: toggleRef, id: labelId, type: "button", "aria-label": t("session_add_tag"), "aria-expanded": open, "aria-controls": listId, className: "flex h-6 touch:-my-2 touch:min-h-10 items-center gap-1 rounded px-1 text-[13px] text-gray-600 hover:text-gray-900", onClick: () => (open ? close() : setOpen(true)), children: [_jsx(StrokeIcon, { name: "plus", size: 14 }), t("session_add_tag")] })] }), open && (_jsxs("div", { ref: panelRef, className: "mt-1 flex flex-col gap-1 rounded-lg border border-gray-50 p-1", children: [_jsx(Input, { autoFocus: !isTouch, "aria-label": t("session_search_tag_placeholder"), "aria-controls": listId, placeholder: t("session_search_tag_placeholder"), value: keyword, onChange: (event) => setKeyword(event.target.value), prefix: _jsx(StrokeIcon, { name: "search", size: 16, className: "text-gray-400" }), className: "rounded-lg border-gray-300 text-[13px] focus-within:border-chat-focus focus-within:shadow-chat-focus-ring", allowClear: true }), _jsxs("ul", { id: listId, role: "listbox", "aria-multiselectable": true, "aria-labelledby": labelId, className: "overflow-y-auto", style: { height: listHeight }, children: [isLoading && (_jsx("li", { className: "flex justify-center py-2", children: _jsx(Spin, { size: "small" }) })), !isLoading && options.length === 0 && (_jsx("li", { className: "px-2 py-1.5 text-[13px] text-gray-600", children: t("session_no_tags") })), options.map((label) => {
53
+ }, children: [_jsxs("button", { ref: toggleRef, id: labelId, type: "button", "aria-label": t("session_add_tag"), "aria-expanded": open, "aria-controls": listId, className: "flex h-9 touch:min-h-10 w-full items-center gap-1 px-3 text-[13px] text-gray-600 hover:text-gray-900", onClick: () => (open ? close() : setOpen(true)), children: [_jsx(StrokeIcon, { name: "plus", size: 14 }), t("session_add_tag")] }), open && (_jsxs("div", { ref: panelRef, className: "mx-2 mb-2 flex flex-col gap-1 rounded-lg border border-gray-50 p-1", children: [_jsx(Input, { autoFocus: !isTouch, "aria-label": t("session_search_tag_placeholder"), "aria-controls": listId, placeholder: t("session_search_tag_placeholder"), value: keyword, onChange: (event) => setKeyword(event.target.value), prefix: _jsx(StrokeIcon, { name: "search", size: 16, className: "text-gray-400" }), className: "rounded-lg border-gray-300 text-[13px] focus-within:border-chat-focus focus-within:shadow-chat-focus-ring", allowClear: true }), _jsxs("ul", { id: listId, role: "listbox", "aria-multiselectable": true, "aria-labelledby": labelId, className: "overflow-y-auto", style: { height: listHeight }, children: [isLoading && (_jsx("li", { className: "flex justify-center py-2", children: _jsx(Spin, { size: "small" }) })), !isLoading && options.length === 0 && (_jsx("li", { className: "px-2 py-1.5 text-[13px] text-gray-600", children: t("session_no_tags") })), options.map((label) => {
57
54
  const isSelected = selectedIds.includes(label.id);
58
55
  return (_jsx("li", { role: "option", "aria-selected": isSelected, children: _jsxs("button", { type: "button", className: `flex h-9 w-full items-center justify-between rounded px-2 text-left text-[13px] text-gray-900 hover:bg-gray-5 ${isSelected ? "bg-primary-25" : ""}`, onClick: () => toggle(label.id), children: [label.name, isSelected && _jsx(StrokeIcon, { name: "check", size: 14 })] }) }, label.id));
59
56
  })] })] }))] }));
@@ -1 +1 @@
1
- {"version":3,"file":"SupportSessionPanel.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SupportSessionPanel.tsx"],"names":[],"mappings":"AAeA,UAAU,wBAAwB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,sFAAsF;IACtF,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED;;;GAGG;AACH,QAAA,MAAM,mBAAmB,GAAI,4FAQ1B,wBAAwB,4CAgG1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"SupportSessionPanel.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SupportSessionPanel.tsx"],"names":[],"mappings":"AAgBA,UAAU,wBAAwB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,sFAAsF;IACtF,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED;;;GAGG;AACH,QAAA,MAAM,mBAAmB,GAAI,4FAQ1B,wBAAwB,4CAiH1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}