Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
feat: ticket comments (#495)
Browse files Browse the repository at this point in the history
* chore: improve `sendMessage` mutation

* feat: ticket comments

* refactor: rename sender to author

* chore: cleanup

* feat: add command menu
  • Loading branch information
DavidRouyer authored Oct 8, 2023
1 parent c3ab2fd commit 8206268
Show file tree
Hide file tree
Showing 33 changed files with 1,018 additions and 152 deletions.
1 change: 1 addition & 0 deletions apps/customer-service/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const config = {
typescript: { ignoreBuildErrors: true },
experimental: {
serverActions: true,
swcPlugins: [['@swc-jotai/react-refresh', {}]],
},
};

Expand Down
3 changes: 3 additions & 0 deletions apps/customer-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
"@trpc/server": "^10.40.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"copy-to-clipboard": "^3.3.3",
"google-libphonenumber": "^3.2.33",
"jotai": "^2.4.3",
"lexical": "^0.12.2",
"lucide-react": "^0.284.0",
"negotiator": "^0.6.3",
Expand All @@ -52,6 +54,7 @@
"@cs/prettier-config": "*",
"@cs/tailwind-config": "*",
"@cs/tsconfig": "*",
"@swc-jotai/react-refresh": "^0.1.0",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",
"@types/google-libphonenumber": "^7.4.27",
Expand Down
5 changes: 4 additions & 1 deletion apps/customer-service/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client';
import { Provider } from 'jotai';
import { SessionProvider } from 'next-auth/react';
import superjson from 'superjson';

Expand Down Expand Up @@ -57,7 +58,9 @@ export function TRPCReactProvider(props: {
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration transformer={superjson}>
<SessionProvider>{props.children}</SessionProvider>
<SessionProvider>
<Provider>{props.children}</Provider>
</SessionProvider>
</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
Expand Down
2 changes: 2 additions & 0 deletions apps/customer-service/src/app/tickets/layout-with-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FC, Suspense, useState } from 'react';
import { AlignJustify } from 'lucide-react';
import { FormattedMessage } from 'react-intl';

import { CommandMenu } from '~/components/command-menu/command-menu';
import { Logo } from '~/components/logo';
import { InboxList } from '~/components/navbar/inbox-list';
import { TeamMemberList } from '~/components/navbar/team-member-list';
Expand Down Expand Up @@ -43,6 +44,7 @@ export const LayoutWithSidebar: FC<{

return (
<div>
<CommandMenu />
<Sheet open={sidebarOpen} onOpenChange={(open) => setSidebarOpen(open)}>
<SheetContent position="left" size="content">
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-background px-6 pb-2">
Expand Down
73 changes: 73 additions & 0 deletions apps/customer-service/src/components/command-menu/command-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';

import { useEffect, useState } from 'react';
import { useSetAtom } from 'jotai';
import { FormattedMessage, useIntl } from 'react-intl';

import { messageModeAtom } from '~/components/messages/message-mode-atom';
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '~/components/ui/command';

export function CommandMenu() {
const { formatMessage } = useIntl();

const [open, setOpen] = useState(false);
const setMessageMode = useSetAtom(messageModeAtom);

useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);

return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder={formatMessage({ id: 'command_menu.search.placeholder' })}
/>
<CommandList>
<CommandEmpty>
<FormattedMessage id="command_menu.no_results" />
</CommandEmpty>
<CommandGroup heading={formatMessage({ id: 'command_menu.commands' })}>
<CommandItem
onSelect={() => {
setMessageMode('comment');
setOpen(false);
}}
>
<span>
<FormattedMessage id="command_menu.commands.write_note" />
</span>
</CommandItem>
<CommandItem
onSelect={() => {
setMessageMode('message');
setOpen(false);
}}
>
<span>
<FormattedMessage id="command_menu.commands.write_message" />
</span>
</CommandItem>
<CommandItem>
<span>
<FormattedMessage id="command_menu.commands.assign_to_me" />
</span>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
208 changes: 126 additions & 82 deletions apps/customer-service/src/components/infos/activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import {
TicketAssignmentChangedWithData,
TicketAssignmentRemovedWithData,
} from '@cs/api/src/router/ticketActivity';
import { TicketActivityType } from '@cs/database/schema/ticketActivity';
import {
TicketActivityType,
TicketCommented,
} from '@cs/database/schema/ticketActivity';

import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar';
import { RelativeTime } from '~/components/ui/relative-time';
import { api } from '~/utils/api';
import { getInitials } from '~/utils/string';
import { cn } from '~/utils/utils';

export const Activity: FC<{
Expand Down Expand Up @@ -38,101 +43,140 @@ export const Activity: FC<{
</div>
{
<>
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-background">
{ticketActivity.type === TicketActivityType.Resolved ? (
<CheckCircle2
className="h-6 w-6 text-valid"
aria-hidden="true"
/>
) : (
<div className="h-1.5 w-1.5 rounded-full bg-gray-100 ring-1 ring-gray-300" />
)}
</div>
<p className="flex-auto py-0.5 text-xs leading-5 text-muted-foreground">
<span className="font-medium text-foreground">
{ticketActivity.author.name}
</span>{' '}
{
{
Created: (
<FormattedMessage id="ticket.activity.type.ticket_created" />
),
Reopened: (
<FormattedMessage id="ticket.activity.type.ticket_reopened" />
),
Resolved: (
<FormattedMessage id="ticket.activity.type.ticket_resolved" />
),
AssignmentAdded: (
<>
{(
ticketActivity.extraInfo as TicketAssignmentAddedWithData
)?.newAssignedToId === ticketActivity.authorId ? (
<FormattedMessage id="ticket.activity.type.ticket_assignment.self_assigned" />
) : (
{ticketActivity.type === TicketActivityType.Commented ? (
<>
<Avatar className="relative mt-3 h-6 w-6 flex-none text-xs">
<AvatarImage
src={ticketActivity.author.avatarUrl ?? undefined}
/>
<AvatarFallback>
{getInitials(ticketActivity.author.name ?? '')}
</AvatarFallback>
</Avatar>
<div className="flex-auto rounded-md p-3 ring-1 ring-inset ring-muted-foreground">
<div className="flex justify-between gap-x-4">
<div className="py-0.5 text-xs leading-5 text-muted-foreground">
<span className="font-medium text-foreground">
{ticketActivity.author.name}
</span>{' '}
<FormattedMessage id="ticket.activity.type.ticket_commented" />
</div>
<time
dateTime={ticketActivity.createdAt.toISOString()}
className="flex-none py-0.5 text-xs leading-5 text-muted-foreground"
>
<RelativeTime
dateTime={new Date(ticketActivity.createdAt)}
/>
</time>
</div>
<p className="text-sm leading-6 text-gray-500">
{(ticketActivity.extraInfo as TicketCommented)?.comment}
</p>
</div>
</>
) : (
<>
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-background">
{TicketActivityType.Resolved ? (
<CheckCircle2
className="h-6 w-6 text-valid"
aria-hidden="true"
/>
) : (
<div className="h-1.5 w-1.5 rounded-full bg-gray-100 ring-1 ring-gray-300" />
)}
</div>

<p className="flex-auto py-0.5 text-xs leading-5 text-muted-foreground">
<span className="font-medium text-foreground">
{ticketActivity.author.name}
</span>{' '}
{
{
AssignmentAdded: (
<>
{(
ticketActivity.extraInfo as TicketAssignmentAddedWithData
)?.newAssignedToId === ticketActivity.authorId ? (
<FormattedMessage id="ticket.activity.type.ticket_assignment.self_assigned" />
) : (
<>
<FormattedMessage id="ticket.activity.type.ticket_assignment.assigned" />{' '}
<span className="font-medium text-foreground">
{
(
ticketActivity.extraInfo as TicketAssignmentAddedWithData
)?.newAssignedTo?.name
}
</span>
</>
)}
</>
),
AssignmentChanged: (
<>
<FormattedMessage id="ticket.activity.type.ticket_assignment.assigned" />{' '}
<span className="font-medium text-foreground">
{
(
ticketActivity.extraInfo as TicketAssignmentAddedWithData
ticketActivity.extraInfo as TicketAssignmentChangedWithData
)?.newAssignedTo?.name
}
</span>
</>
)}
</>
),
AssignmentChanged: (
<>
<FormattedMessage id="ticket.activity.type.ticket_assignment.assigned" />{' '}
<span className="font-medium text-foreground">
{
(
ticketActivity.extraInfo as TicketAssignmentChangedWithData
)?.newAssignedTo?.name
}
</span>{' '}
<FormattedMessage id="ticket.activity.type.ticket_assignment.and_unassigned" />{' '}
<span className="font-medium text-foreground">
{
(
ticketActivity.extraInfo as TicketAssignmentChangedWithData
)?.oldAssignedTo?.name
}
</span>
</>
),
AssignmentRemoved: (
<>
{(
ticketActivity.extraInfo as TicketAssignmentRemovedWithData
)?.oldAssignedToId === ticketActivity.authorId ? (
<FormattedMessage id="ticket.activity.type.ticket_assignment.self_unassigned" />
) : (
<>
<FormattedMessage id="ticket.activity.type.ticket_assignment.unassigned" />{' '}
</span>{' '}
<FormattedMessage id="ticket.activity.type.ticket_assignment.and_unassigned" />{' '}
<span className="font-medium text-foreground">
{
(
ticketActivity.extraInfo as TicketAssignmentRemovedWithData
ticketActivity.extraInfo as TicketAssignmentChangedWithData
)?.oldAssignedTo?.name
}
</span>
</>
)}
</>
),
}[ticketActivity.type]
}
.
</p>
<time
dateTime={ticketActivity.createdAt.toISOString()}
className="flex-none py-0.5 text-xs leading-5 text-muted-foreground"
>
<RelativeTime dateTime={new Date(ticketActivity.createdAt)} />
</time>
),
AssignmentRemoved: (
<>
{(
ticketActivity.extraInfo as TicketAssignmentRemovedWithData
)?.oldAssignedToId === ticketActivity.authorId ? (
<FormattedMessage id="ticket.activity.type.ticket_assignment.self_unassigned" />
) : (
<>
<FormattedMessage id="ticket.activity.type.ticket_assignment.unassigned" />{' '}
<span className="font-medium text-foreground">
{
(
ticketActivity.extraInfo as TicketAssignmentRemovedWithData
)?.oldAssignedTo?.name
}
</span>
</>
)}
</>
),
Created: (
<FormattedMessage id="ticket.activity.type.ticket_created" />
),
Reopened: (
<FormattedMessage id="ticket.activity.type.ticket_reopened" />
),
Resolved: (
<FormattedMessage id="ticket.activity.type.ticket_resolved" />
),
}[ticketActivity.type]
}
.
</p>
<time
dateTime={ticketActivity.createdAt.toISOString()}
className="flex-none py-0.5 text-xs leading-5 text-muted-foreground"
>
<RelativeTime
dateTime={new Date(ticketActivity.createdAt)}
/>
</time>
</>
)}
</>
}
</li>
Expand Down
Loading

0 comments on commit 8206268

Please sign in to comment.