Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
72eeaa3
Inbox mail view and create modal
lumburovskalina Nov 6, 2025
f68c103
Draft display for inbox mail
lumburovskalina Nov 6, 2025
13e8f14
Component inbox mail moved
lumburovskalina Nov 7, 2025
bc5fbd5
merge
LennartSchmidtKern Nov 11, 2025
c90fd04
inbox mail reworks
LennartSchmidtKern Nov 12, 2025
15ab4f7
Clean up
LennartSchmidtKern Nov 17, 2025
f1fcda6
unread improvements, fixes
LennartSchmidtKern Nov 19, 2025
1b057a7
admin dashboard compatible inbox mail
LennartSchmidtKern Nov 19, 2025
5fba32c
local translation mapping and org selection admins inbox mail
LennartSchmidtKern Nov 19, 2025
8de52e9
rework translations inbox mail
LennartSchmidtKern Nov 20, 2025
110807e
auto scroll fix
LennartSchmidtKern Nov 20, 2025
4eb8ac6
translator injection
LennartSchmidtKern Nov 20, 2025
bff3a13
org selection improvements
LennartSchmidtKern Nov 20, 2025
3342598
new mail counter hook
LennartSchmidtKern Nov 20, 2025
98522e5
Merge branch 'inbox-mail' of github.com:code-kern-ai/submodule-react-…
LennartSchmidtKern Nov 20, 2025
f4e2b52
inbox mail title badge
LennartSchmidtKern Nov 20, 2025
5522b0b
improve translation handling
LennartSchmidtKern Nov 20, 2025
2afaaf4
t new modal
LennartSchmidtKern Nov 20, 2025
bfefca1
add admin panel inbox mail
LennartSchmidtKern Nov 20, 2025
bfbcbde
typing
LennartSchmidtKern Nov 20, 2025
ac8e9fb
imrpoved refresh new mails and users
LennartSchmidtKern Nov 24, 2025
a4c53ba
auto assign org on jump to inbox mail convo
LennartSchmidtKern Nov 25, 2025
8fb7dd3
fix and clean up
LennartSchmidtKern Nov 25, 2025
ec1fb24
refresh optional
LennartSchmidtKern Nov 25, 2025
a026911
fix to
LennartSchmidtKern Nov 25, 2025
cdc4b07
PR comments
LennartSchmidtKern Nov 27, 2025
ee8c706
PR comments
LennartSchmidtKern Nov 27, 2025
23b956d
translator types
LennartSchmidtKern Nov 27, 2025
928f3cf
fix refetch inbox mail new and PR comments
LennartSchmidtKern Nov 27, 2025
c7d028e
imrpove refetch logic
LennartSchmidtKern Nov 27, 2025
a14efda
PR comments
lumburovskalina Dec 1, 2025
ceb7a05
PR comments
LennartSchmidtKern Dec 1, 2025
53bb7b0
Merge branch 'inbox-mail' of github.com:code-kern-ai/submodule-react-…
LennartSchmidtKern Dec 1, 2025
68432d2
local translations
LennartSchmidtKern Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
421 changes: 421 additions & 0 deletions components/inbox-mail/CreateNewMailModal.tsx

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions components/inbox-mail/DeleteInboxMailModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Dialog, Transition } from "@headlessui/react";
import { IconAlertTriangle } from "@tabler/icons-react";
import { Fragment, useMemo, useRef } from "react";

interface ConfirmDeleteModalProps {
open: boolean;
setOpen: (open: boolean) => void;
onConfirm: () => void;
translator: (key: string) => string;
}


export default function DeleteInboxMailModal(props: ConfirmDeleteModalProps) {

const cancelRef = useRef(null);
const t = useMemo(() => props.translator, [props.translator]);

return (
<Transition.Root show={props.open} as={Fragment}>
<Dialog as="div" className="relative z-50" initialFocus={cancelRef} onClose={props.setOpen ? props.setOpen : () => null}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full justify-center p-4 items-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="flex justify-center w-full">
<div className="w-full max-w-md">
<Dialog.Panel className="relative rounded-lg bg-white shadow-xl sm:my-8">
<div className="p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center
rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<IconAlertTriangle className="h-6 w-6 text-red-600" />
</div>

<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
{t("inboxMail.deleteHeader")}
</Dialog.Title>

<p className="mt-2 text-sm text-gray-600">{t("inboxMail.deleteConfirmation")}</p>
</div>
</div>
<div className="mt-6 sm:flex sm:flex-row-reverse gap-3">
<button
onClick={() => {
props.onConfirm();
props.setOpen(false);
}}
className="inline-flex w-full justify-center rounded-md border border-transparent
bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm
hover:bg-red-700 sm:ml-3 sm:w-auto sm:text-sm"
>
{t("inboxMail.deleteButton")}
</button>
<button
ref={cancelRef}
onClick={() => props.setOpen(false)}
className="inline-flex w-full justify-center rounded-md border border-gray-300
bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm
hover:bg-gray-50 sm:w-auto sm:text-sm"
>
{t("inboxMail.cancelButton")}
</button>
</div>
</div>
</Dialog.Panel>
</div>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
129 changes: 129 additions & 0 deletions components/inbox-mail/InboxMailAdminPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { useCallback } from "react";
import { InboxMailThread, InboxMailThreadSupportProgressState, User } from "./types-mail";
import { IconExternalLink, IconProgressCheck } from "@tabler/icons-react";
import KernDropdown from "../KernDropdown";
import { addUserToOrganization, removeUserFromOrganization } from "./service-mail";

interface ProgressOption {
name: string;
value: InboxMailThreadSupportProgressState;
}

interface InboxMailAdminPanelProps {
selectedThread: InboxMailThread;
progressStateOptions: ProgressOption[];
handleInboxMailProgressChange: (value: InboxMailThreadSupportProgressState) => void;
currentUser: User;
}

function InboxMailAdminPanel(props: InboxMailAdminPanelProps) {
// No translations needed, admin only
const assignAndJump = useCallback((toConversation: boolean) => {
if (!props.currentUser) return;
const currentOrganizationId = props.currentUser?.organizationId;
if (!currentOrganizationId) {
addUserToOrganization(props.currentUser.mail, props.selectedThread.organizationName, (res) => {
jumptoConversationOrProject(toConversation);

});
} else if (currentOrganizationId === props.selectedThread.organizationId) {
jumptoConversationOrProject(toConversation);

} else {
removeUserFromOrganization(props.currentUser.mail, (res) => {
addUserToOrganization(props.currentUser.mail, props.selectedThread.organizationName, (res) => {
jumptoConversationOrProject(toConversation);
});
});
}
}, [props.currentUser, props.selectedThread]);

const jumptoConversationOrProject = useCallback((toConversation: boolean) => {
if (toConversation) {
window.open(`/cognition/projects/${props.selectedThread.metaData?.projectId}/ui/${props.selectedThread.metaData?.conversationId}`, '_blank');
}
else {
window.open(`/cognition/projects/${props.selectedThread.metaData.projectId}/pipeline`, '_blank');
}
}, [props.selectedThread.metaData]);

return (
<div>
<div className="flex items-center gap-x-2">
<div className="flex items-center gap-x-1 mb-2 mt-1 overflow-y-visible z-10">
<span className="text-sm font-medium mr-2">Progress:</span>
<KernDropdown
dropdownWidth="w-40"
buttonName={
props.progressStateOptions.find(
(option) => option.value === props.selectedThread.progressState
)?.name || "Set progress"
}
options={props.progressStateOptions}
selectedOption={(option: { label: string; value: InboxMailThreadSupportProgressState }) =>
props.handleInboxMailProgressChange(option.value)
}
/>
</div>
{props.selectedThread.progressState !== InboxMailThreadSupportProgressState.PENDING && props.selectedThread.metaData?.supportOwnerName && (
<div className="bg-orange-400 text-white rounded-full px-2 py-0.5 text-xs flex items-center gap-x-2 ml-2">
<IconProgressCheck className="w-5 h-5" />
{props.selectedThread.metaData.supportOwnerName.first}{" "}
{props.selectedThread.metaData.supportOwnerName.last}
</div>
)}
</div>

{props.selectedThread.metaData?.projectId && (
<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">
<div className="flex items-center gap-1.5 text-xs">
<span className="font-semibold">Project</span>
</div>

<div className="flex items-center gap-1.5 text-xs bg-slate-400 px-2 py-0.5 rounded-md">
<span>ID:</span>
<span>{props.selectedThread.metaData.projectId}</span>
</div>
<div className="flex items-center gap-1.5 text-xs bg-slate-400 px-2 py-0.5 rounded-md">
<span>{props.selectedThread.metaData.projectName}</span>
</div>
<button
className="flex items-center gap-1.5 text-xs bg-slate-400 px-2 py-0.5 rounded-md"
onClick={() =>
assignAndJump(false)
}
>
<IconExternalLink className="w-4 h-4" />
</button>
</div>
)
}

{
props.selectedThread.metaData?.conversationId && (
<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">
<div className="flex items-center gap-1.5 text-xs">
<span className="font-semibold">Conversation</span>
</div>

<div className="flex items-center gap-1.5 text-xs bg-gray-400 px-2 py-0.5 rounded-md">
<span>ID:</span>
<span>{props.selectedThread.metaData.conversationId}</span>
</div>
<div className="flex items-center gap-1.5 text-xs bg-gray-400 px-2 py-0.5 rounded-md">
<span>{props.selectedThread.metaData.conversationHeader || "N/A"}</span>
</div>
<button
className="flex items-center gap-1.5 text-xs bg-gray-400 px-2 py-0.5 rounded-md"
onClick={() => assignAndJump(true)}
>
<IconExternalLink className="w-4 h-4" />
</button>
</div>
)
}
</div >
);
};

export default InboxMailAdminPanel;
69 changes: 69 additions & 0 deletions components/inbox-mail/InboxMailItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { IconTrash } from "@tabler/icons-react";
import KernButton from "../kern-button/KernButton";
import DeleteInboxMailModal from "./DeleteInboxMailModal";
import { InboxMail, User } from "./types-mail";
import { useCallback, useMemo, useState } from "react";
import { formatDisplayTimestampFull } from "@/submodules/javascript-functions/date-parser";

interface ThreadMailItemProps {
mail: InboxMail;
currentUser: User;
translator: (key: string) => string;
onDelete?: (id: string) => void;
}

export default function ThreadMailItem(props: ThreadMailItemProps) {

const t = useMemo(() => props.translator, [props.translator]);
const [openDeleteConfirm, setOpenDeleteConfirm] = useState(false);
const handleConfirmDelete = useCallback(() => {
if (props.onDelete) {
props.onDelete(props.mail.id);
}
}, [props.mail.id, props.onDelete]);

return (
<>
<div className="py-3 px-4 mb-2 border border-gray-300 rounded-lg shadow-sm bg-white">
<div className="flex items-center justify-between text-sm">
<div className="grow min-w-0">
<div className="flex items-center justify-between gap-x-2 flex-nowrap">
<span className="font-medium text-gray-700">
{props.mail.senderName?.first} {props.mail.senderName?.last}
</span>

<span className="ml-auto text-xs text-gray-400 whitespace-nowrap">
{formatDisplayTimestampFull(props.mail.createdAt)}
</span>
</div>

<span className="text-sm text-gray-500">
{t("inboxMail.to")}: {props.mail.recipientNames.map((name) => `${name.first} ${name.last}`).join(", ")}
</span>
</div>
</div>

<div className="border-t border-gray-200 mt-2 pt-2 text-gray-700 whitespace-pre-line break-words">
{props.mail.content}
</div>

<div className="mt-2 flex items-center justify-end space-x-3">
{props.currentUser.id === props.mail.senderId && (
<KernButton
icon={IconTrash}
size="small"
className="text-gray-700 hover:text-red-700"
onClick={() => setOpenDeleteConfirm(true)}
/>
)}
</div>
</div>
<DeleteInboxMailModal
open={openDeleteConfirm}
setOpen={setOpenDeleteConfirm}
onConfirm={handleConfirmDelete}
translator={props.translator}
/>
</>
);
}
73 changes: 73 additions & 0 deletions components/inbox-mail/InboxMailNavigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { MemoIconMail } from "@/submodules/react-components/components/kern-icons/icons";
import { useRouter } from "next/router";
import tinycolor from 'tinycolor2'
import { useNewMailCount } from "./helper";
import { useCallback, useMemo } from "react";

type InboxMailNavigatorProps = {
forChatArea?: boolean;
project?: { customerColorPrimary: string; id: string; };
chatId?: string;
refreshToken?: any;
}

export default function InboxMailNavigator(props: InboxMailNavigatorProps) {
const router = useRouter();

const navigateToMailPage = useCallback(() => {
const chatIdParam = props.chatId ? `?chatId=${props.chatId}` : '';
const projectIdParam = props.project ? props.chatId ? `&projectId=${props.project.id}` : `?projectId=${props.project.id}` : '';
router.push(`/inbox-mail${chatIdParam}${projectIdParam}`);
}, [props.chatId, props.project]);

const isLightDesign = useMemo(() => tinycolor(props.project?.customerColorPrimary).isLight(), [props.project?.customerColorPrimary]);

const buttonClasses = useMemo(() => {
if (props.forChatArea) {
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"
if (isLightDesign) return 'bg-gray-100 text-gray-700 border-gray-300 ' + classes;
else return 'bg-zinc-900 text-zinc-100 border-zinc-700 ' + classes;
}
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"
}, [props.forChatArea, isLightDesign]);

return <div className="relative">
<button className={buttonClasses} onClick={navigateToMailPage}>
<MemoIconMail />
<InboxMailBadge forChatArea={props.forChatArea} refreshToken={props.refreshToken} />
</button>
</div>
}



interface NewMailBadgeProps {
forChatArea?: boolean;
refreshInterval?: number; // optional, default to 60000ms
refreshToken?: any; // optional, to trigger refresh when changed
}

export function InboxMailBadge(props: NewMailBadgeProps) {
const newMailCount = useNewMailCount(props.refreshInterval, props.refreshToken);

if (newMailCount === 0) return;

const badgeClasses = props.forChatArea
? 'top-0 right-0'
: 'top-1 right-1';

return (
<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}`}>
{newMailCount}
</div>
);
}

export function InboxMailTitleBadge(props: { newMailCount?: number, refreshToken?: any }) {
return (
<div className="relative inline-flex items-center pr-5">
<span >Inbox Mail</span>
<InboxMailBadge forChatArea={false} refreshToken={props.refreshToken} />
</div>
);
}
Loading