Skip to content

Commit ec0b9e0

Browse files
Inbox mail view and create modal (#63)
* Inbox mail view and create modal * Draft display for inbox mail * Component inbox mail moved * inbox mail reworks * Clean up * unread improvements, fixes * admin dashboard compatible inbox mail * local translation mapping and org selection admins inbox mail * rework translations inbox mail * auto scroll fix * translator injection * org selection improvements * new mail counter hook * inbox mail title badge * improve translation handling * t new modal * add admin panel inbox mail * typing * imrpoved refresh new mails and users * auto assign org on jump to inbox mail convo * fix and clean up * refresh optional * fix to * PR comments * PR comments * translator types * fix refetch inbox mail new and PR comments * imrpove refetch logic * PR comments * PR comments * local translations --------- Co-authored-by: LennartSchmidtKern <lennart.schmidt@kern.ai>
1 parent 367263c commit ec0b9e0

15 files changed

+1629
-3
lines changed

components/inbox-mail/CreateNewMailModal.tsx

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Dialog, Transition } from "@headlessui/react";
2+
import { IconAlertTriangle } from "@tabler/icons-react";
3+
import { Fragment, useMemo, useRef } from "react";
4+
5+
interface ConfirmDeleteModalProps {
6+
open: boolean;
7+
setOpen: (open: boolean) => void;
8+
onConfirm: () => void;
9+
translator: (key: string) => string;
10+
}
11+
12+
13+
export default function DeleteInboxMailModal(props: ConfirmDeleteModalProps) {
14+
15+
const cancelRef = useRef(null);
16+
const t = useMemo(() => props.translator, [props.translator]);
17+
18+
return (
19+
<Transition.Root show={props.open} as={Fragment}>
20+
<Dialog as="div" className="relative z-50" initialFocus={cancelRef} onClose={props.setOpen ? props.setOpen : () => null}>
21+
<Transition.Child
22+
as={Fragment}
23+
enter="ease-out duration-300"
24+
enterFrom="opacity-0"
25+
enterTo="opacity-100"
26+
leave="ease-in duration-200"
27+
leaveFrom="opacity-100"
28+
leaveTo="opacity-0"
29+
>
30+
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
31+
</Transition.Child>
32+
33+
<div className="fixed inset-0 overflow-y-auto">
34+
<div className="flex min-h-full justify-center p-4 items-center">
35+
<Transition.Child
36+
as={Fragment}
37+
enter="ease-out duration-300"
38+
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
39+
enterTo="opacity-100 translate-y-0 sm:scale-100"
40+
leave="ease-in duration-200"
41+
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
42+
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
43+
>
44+
<div className="flex justify-center w-full">
45+
<div className="w-full max-w-md">
46+
<Dialog.Panel className="relative rounded-lg bg-white shadow-xl sm:my-8">
47+
<div className="p-6">
48+
<div className="sm:flex sm:items-start">
49+
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center
50+
rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
51+
<IconAlertTriangle className="h-6 w-6 text-red-600" />
52+
</div>
53+
54+
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
55+
<Dialog.Title
56+
as="h3"
57+
className="text-lg font-medium leading-6 text-gray-900"
58+
>
59+
{t("inboxMail.deleteHeader")}
60+
</Dialog.Title>
61+
62+
<p className="mt-2 text-sm text-gray-600">{t("inboxMail.deleteConfirmation")}</p>
63+
</div>
64+
</div>
65+
<div className="mt-6 sm:flex sm:flex-row-reverse gap-3">
66+
<button
67+
onClick={() => {
68+
props.onConfirm();
69+
props.setOpen(false);
70+
}}
71+
className="inline-flex w-full justify-center rounded-md border border-transparent
72+
bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm
73+
hover:bg-red-700 sm:ml-3 sm:w-auto sm:text-sm"
74+
>
75+
{t("inboxMail.deleteButton")}
76+
</button>
77+
<button
78+
ref={cancelRef}
79+
onClick={() => props.setOpen(false)}
80+
className="inline-flex w-full justify-center rounded-md border border-gray-300
81+
bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm
82+
hover:bg-gray-50 sm:w-auto sm:text-sm"
83+
>
84+
{t("inboxMail.cancelButton")}
85+
</button>
86+
</div>
87+
</div>
88+
</Dialog.Panel>
89+
</div>
90+
</div>
91+
</Transition.Child>
92+
</div>
93+
</div>
94+
</Dialog>
95+
</Transition.Root>
96+
);
97+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React, { useCallback } from "react";
2+
import { InboxMailThread, InboxMailThreadSupportProgressState, User } from "./types-mail";
3+
import { IconExternalLink, IconProgressCheck } from "@tabler/icons-react";
4+
import KernDropdown from "../KernDropdown";
5+
import { addUserToOrganization, removeUserFromOrganization } from "./service-mail";
6+
7+
interface ProgressOption {
8+
name: string;
9+
value: InboxMailThreadSupportProgressState;
10+
}
11+
12+
interface InboxMailAdminPanelProps {
13+
selectedThread: InboxMailThread;
14+
progressStateOptions: ProgressOption[];
15+
handleInboxMailProgressChange: (value: InboxMailThreadSupportProgressState) => void;
16+
currentUser: User;
17+
}
18+
19+
function InboxMailAdminPanel(props: InboxMailAdminPanelProps) {
20+
// No translations needed, admin only
21+
const assignAndJump = useCallback((toConversation: boolean) => {
22+
if (!props.currentUser) return;
23+
const currentOrganizationId = props.currentUser?.organizationId;
24+
if (!currentOrganizationId) {
25+
addUserToOrganization(props.currentUser.mail, props.selectedThread.organizationName, (res) => {
26+
jumptoConversationOrProject(toConversation);
27+
28+
});
29+
} else if (currentOrganizationId === props.selectedThread.organizationId) {
30+
jumptoConversationOrProject(toConversation);
31+
32+
} else {
33+
removeUserFromOrganization(props.currentUser.mail, (res) => {
34+
addUserToOrganization(props.currentUser.mail, props.selectedThread.organizationName, (res) => {
35+
jumptoConversationOrProject(toConversation);
36+
});
37+
});
38+
}
39+
}, [props.currentUser, props.selectedThread]);
40+
41+
const jumptoConversationOrProject = useCallback((toConversation: boolean) => {
42+
if (toConversation) {
43+
window.open(`/cognition/projects/${props.selectedThread.metaData?.projectId}/ui/${props.selectedThread.metaData?.conversationId}`, '_blank');
44+
}
45+
else {
46+
window.open(`/cognition/projects/${props.selectedThread.metaData.projectId}/pipeline`, '_blank');
47+
}
48+
}, [props.selectedThread.metaData]);
49+
50+
return (
51+
<div>
52+
<div className="flex items-center gap-x-2">
53+
<div className="flex items-center gap-x-1 mb-2 mt-1 overflow-y-visible z-10">
54+
<span className="text-sm font-medium mr-2">Progress:</span>
55+
<KernDropdown
56+
dropdownWidth="w-40"
57+
buttonName={
58+
props.progressStateOptions.find(
59+
(option) => option.value === props.selectedThread.progressState
60+
)?.name || "Set progress"
61+
}
62+
options={props.progressStateOptions}
63+
selectedOption={(option: { label: string; value: InboxMailThreadSupportProgressState }) =>
64+
props.handleInboxMailProgressChange(option.value)
65+
}
66+
/>
67+
</div>
68+
{props.selectedThread.progressState !== InboxMailThreadSupportProgressState.PENDING && props.selectedThread.metaData?.supportOwnerName && (
69+
<div className="bg-orange-400 text-white rounded-full px-2 py-0.5 text-xs flex items-center gap-x-2 ml-2">
70+
<IconProgressCheck className="w-5 h-5" />
71+
{props.selectedThread.metaData.supportOwnerName.first}{" "}
72+
{props.selectedThread.metaData.supportOwnerName.last}
73+
</div>
74+
)}
75+
</div>
76+
77+
{props.selectedThread.metaData?.projectId && (
78+
<div className="flex items-center gap-3 ml-2 my-2 px-3 py-1 rounded-xl bg-slate-400/60 text-white w-fit">
79+
<div className="flex items-center gap-1.5 text-xs">
80+
<span className="font-semibold">Project</span>
81+
</div>
82+
83+
<div className="flex items-center gap-1.5 text-xs bg-slate-400 px-2 py-0.5 rounded-md">
84+
<span>ID:</span>
85+
<span>{props.selectedThread.metaData.projectId}</span>
86+
</div>
87+
<div className="flex items-center gap-1.5 text-xs bg-slate-400 px-2 py-0.5 rounded-md">
88+
<span>{props.selectedThread.metaData.projectName}</span>
89+
</div>
90+
<button
91+
className="flex items-center gap-1.5 text-xs bg-slate-400 px-2 py-0.5 rounded-md"
92+
onClick={() =>
93+
assignAndJump(false)
94+
}
95+
>
96+
<IconExternalLink className="w-4 h-4" />
97+
</button>
98+
</div>
99+
)
100+
}
101+
102+
{
103+
props.selectedThread.metaData?.conversationId && (
104+
<div className="flex items-center gap-3 ml-2 my-2 px-3 py-1 rounded-xl bg-gray-400/60 text-white w-fit">
105+
<div className="flex items-center gap-1.5 text-xs">
106+
<span className="font-semibold">Conversation</span>
107+
</div>
108+
109+
<div className="flex items-center gap-1.5 text-xs bg-gray-400 px-2 py-0.5 rounded-md">
110+
<span>ID:</span>
111+
<span>{props.selectedThread.metaData.conversationId}</span>
112+
</div>
113+
<div className="flex items-center gap-1.5 text-xs bg-gray-400 px-2 py-0.5 rounded-md">
114+
<span>{props.selectedThread.metaData.conversationHeader || "N/A"}</span>
115+
</div>
116+
<button
117+
className="flex items-center gap-1.5 text-xs bg-gray-400 px-2 py-0.5 rounded-md"
118+
onClick={() => assignAndJump(true)}
119+
>
120+
<IconExternalLink className="w-4 h-4" />
121+
</button>
122+
</div>
123+
)
124+
}
125+
</div >
126+
);
127+
};
128+
129+
export default InboxMailAdminPanel;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { IconTrash } from "@tabler/icons-react";
2+
import KernButton from "../kern-button/KernButton";
3+
import DeleteInboxMailModal from "./DeleteInboxMailModal";
4+
import { InboxMail, User } from "./types-mail";
5+
import { useCallback, useMemo, useState } from "react";
6+
import { formatDisplayTimestampFull } from "@/submodules/javascript-functions/date-parser";
7+
8+
interface ThreadMailItemProps {
9+
mail: InboxMail;
10+
currentUser: User;
11+
translator: (key: string) => string;
12+
onDelete?: (id: string) => void;
13+
}
14+
15+
export default function ThreadMailItem(props: ThreadMailItemProps) {
16+
17+
const t = useMemo(() => props.translator, [props.translator]);
18+
const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false);
19+
const handleConfirmDelete = useCallback(() => {
20+
if (props.onDelete) {
21+
props.onDelete(props.mail.id);
22+
}
23+
}, [props.mail.id, props.onDelete]);
24+
25+
return (
26+
<>
27+
<div className="py-3 px-4 mb-2 border border-gray-300 rounded-lg shadow-sm bg-white">
28+
<div className="flex items-center justify-between text-sm">
29+
<div className="grow min-w-0">
30+
<div className="flex items-center justify-between gap-x-2 flex-nowrap">
31+
<span className="font-medium text-gray-700">
32+
{props.mail.senderName?.first} {props.mail.senderName?.last}
33+
</span>
34+
35+
<span className="ml-auto text-xs text-gray-400 whitespace-nowrap">
36+
{formatDisplayTimestampFull(props.mail.createdAt)}
37+
</span>
38+
</div>
39+
40+
<span className="text-sm text-gray-500">
41+
{t("inboxMail.to")}: {props.mail.recipientNames.map((name) => `${name.first} ${name.last}`).join(", ")}
42+
</span>
43+
</div>
44+
</div>
45+
46+
<div className="border-t border-gray-200 mt-2 pt-2 text-gray-700 whitespace-pre-line break-words">
47+
{props.mail.content}
48+
</div>
49+
50+
<div className="mt-2 flex items-center justify-end space-x-3">
51+
{props.currentUser.id === props.mail.senderId && (
52+
<KernButton
53+
icon={IconTrash}
54+
size="small"
55+
className="text-gray-700 hover:text-red-700"
56+
onClick={() => setOpenDeleteConfirm(true)}
57+
/>
58+
)}
59+
</div>
60+
</div>
61+
<DeleteInboxMailModal
62+
open={openDeleteConfirm}
63+
setOpen={setOpenDeleteConfirm}
64+
onConfirm={handleConfirmDelete}
65+
translator={props.translator}
66+
/>
67+
</>
68+
);
69+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { MemoIconMail } from "@/submodules/react-components/components/kern-icons/icons";
2+
import { useRouter } from "next/router";
3+
import tinycolor from 'tinycolor2'
4+
import { useNewMailCount } from "./helper";
5+
import { useCallback, useMemo } from "react";
6+
7+
type InboxMailNavigatorProps = {
8+
forChatArea?: boolean;
9+
project?: { customerColorPrimary: string; id: string; };
10+
chatId?: string;
11+
refreshToken?: any;
12+
}
13+
14+
export default function InboxMailNavigator(props: InboxMailNavigatorProps) {
15+
const router = useRouter();
16+
17+
const navigateToMailPage = useCallback(() => {
18+
const chatIdParam = props.chatId ? `?chatId=${props.chatId}` : '';
19+
const projectIdParam = props.project ? props.chatId ? `&projectId=${props.project.id}` : `?projectId=${props.project.id}` : '';
20+
router.push(`/inbox-mail${chatIdParam}${projectIdParam}`);
21+
}, [props.chatId, props.project]);
22+
23+
const isLightDesign = useMemo(() => tinycolor(props.project?.customerColorPrimary).isLight(), [props.project?.customerColorPrimary]);
24+
25+
const buttonClasses = useMemo(() => {
26+
if (props.forChatArea) {
27+
const classes = "items-center justify-center w-8 h-8 border group flex -x-3 rounded-md p-1 text-sm leading-6 font-semibold"
28+
if (isLightDesign) return 'bg-gray-100 text-gray-700 border-gray-300 ' + classes;
29+
else return 'bg-zinc-900 text-zinc-100 border-zinc-700 ' + classes;
30+
}
31+
return "text-gray-400 hover:text-green-600 hover:bg-zinc-800 border-gray-700 items-center justify-center w-10 h-10 border group flex -x-3 rounded-md p-2 text-sm leading-6 font-semibold"
32+
}, [props.forChatArea, isLightDesign]);
33+
34+
return <div className="relative">
35+
<button className={buttonClasses} onClick={navigateToMailPage}>
36+
<MemoIconMail />
37+
<InboxMailBadge forChatArea={props.forChatArea} refreshToken={props.refreshToken} />
38+
</button>
39+
</div>
40+
}
41+
42+
43+
44+
interface NewMailBadgeProps {
45+
forChatArea?: boolean;
46+
refreshInterval?: number; // optional, default to 60000ms
47+
refreshToken?: any; // optional, to trigger refresh when changed
48+
}
49+
50+
export function InboxMailBadge(props: NewMailBadgeProps) {
51+
const newMailCount = useNewMailCount(props.refreshInterval, props.refreshToken);
52+
53+
if (newMailCount === 0) return;
54+
55+
const badgeClasses = props.forChatArea
56+
? 'top-0 right-0'
57+
: 'top-1 right-1';
58+
59+
return (
60+
<div className={`absolute flex items-center justify-center w-3 h-3 bg-red-500 rounded-full text-white text-[0.625rem] font-bold pointer-events-none ${badgeClasses}`}>
61+
{newMailCount}
62+
</div>
63+
);
64+
}
65+
66+
export function InboxMailTitleBadge(props: { newMailCount?: number, refreshToken?: any }) {
67+
return (
68+
<div className="relative inline-flex items-center pr-5">
69+
<span >Inbox Mail</span>
70+
<InboxMailBadge forChatArea={false} refreshToken={props.refreshToken} />
71+
</div>
72+
);
73+
}

0 commit comments

Comments
 (0)