Skip to content

Commit b2ec8ce

Browse files
committed
feat: case manager v3 - new inboxes page
1 parent 49a28ed commit b2ec8ce

23 files changed

Lines changed: 1330 additions & 380 deletions

File tree

packages/app-builder/src/components/Cases/CasesList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SelectionProps } from '@app-builder/hooks/useListSelection';
1+
import { SelectionProps } from '@app-builder/hooks/useTanstackTableListSelection';
22
import { type Case } from '@app-builder/models/cases';
33
import { useOrganizationTags } from '@app-builder/services/organization/organization-tags';
44
import {

packages/app-builder/src/components/Cases/CreateCase.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { useGetInboxesQuery } from '@app-builder/queries/cases/get-inboxes';
1212
import { getFieldErrors } from '@app-builder/utils/form';
1313
import { useForm } from '@tanstack/react-form';
14+
import { useQueryClient } from '@tanstack/react-query';
1415
import { useTranslation } from 'react-i18next';
1516
import { Button, Select } from 'ui-design-system';
1617
import { Icon } from 'ui-icons';
@@ -21,6 +22,7 @@ export function CreateCase() {
2122
const createCaseMutation = useCreateCaseMutation();
2223
const { data } = useCaseRightPanelContext();
2324
const revalidate = useLoaderRevalidator();
25+
const queryClient = useQueryClient();
2426

2527
const form = useForm({
2628
defaultValues: {
@@ -30,6 +32,7 @@ export function CreateCase() {
3032
onSubmit: ({ value, formApi }) => {
3133
if (formApi.state.isValid) {
3234
createCaseMutation.mutateAsync(value).then((res) => {
35+
queryClient.invalidateQueries({ queryKey: ['cases', 'get-cases', value.inboxId] });
3336
revalidate();
3437
});
3538
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { User } from '@app-builder/models';
2+
import { CaseContributor } from '@app-builder/models/cases';
3+
import { useOrganizationUsers } from '@app-builder/services/organization/organization-users';
4+
import { getFullName } from '@app-builder/services/user';
5+
import { useTranslation } from 'react-i18next';
6+
import { Avatar, cn, Tooltip } from 'ui-design-system';
7+
8+
export const AssignedContributors = ({
9+
assignedTo,
10+
contributors,
11+
}: {
12+
assignedTo: string | undefined;
13+
contributors: CaseContributor[];
14+
}) => {
15+
const { getOrgUserById } = useOrganizationUsers();
16+
const assignedUser = assignedTo ? getOrgUserById(assignedTo) : undefined;
17+
const contributorsUsers = contributors.map((contributor) => getOrgUserById(contributor.userId));
18+
19+
return (
20+
<div className="inline-flex items-center gap-v2-sm">
21+
{assignedTo ? <AvatarWithTooltip user={assignedUser} className="border-purple-65" /> : null}
22+
<span className="lg:flex items-center gap-v2-xs group/contributors hidden">
23+
{contributorsUsers.map((user, idx) =>
24+
user ? (
25+
<div
26+
key={user.userId}
27+
className="w-4 group-hover/contributors:w-9 rotate-0 overflow-visible transition-all"
28+
>
29+
<AvatarWithTooltip user={user} />
30+
</div>
31+
) : null,
32+
)}
33+
</span>
34+
</div>
35+
);
36+
};
37+
38+
type AvatarWithTooltipProps = {
39+
user: User | undefined;
40+
className?: string;
41+
};
42+
43+
export const AvatarWithTooltip = ({ user, className }: AvatarWithTooltipProps) => {
44+
const { t } = useTranslation(['cases']);
45+
const avatar = (
46+
<Avatar key={user?.userId} size="s" firstName={user?.firstName} lastName={user?.lastName} />
47+
);
48+
49+
return (
50+
<Tooltip.Default
51+
content={
52+
<div key={user?.userId ?? 0} className="flex flex-row items-center gap-1">
53+
{avatar}
54+
<div className="text-grey-00 text-xs font-normal capitalize">
55+
{getFullName(user) || t('cases:case_detail.unknown_user')}
56+
</div>
57+
</div>
58+
}
59+
>
60+
<div className="flex w-fit flex-row items-center gap-1">
61+
<span className={cn('border-2 border-white rounded-full', className)}>{avatar}</span>
62+
</div>
63+
</Tooltip.Default>
64+
);
65+
};
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { MultiSelect } from '@app-builder/components/MultiSelect';
2+
import { useOrganizationTags } from '@app-builder/services/organization/organization-tags';
3+
import { formatDateRelative } from '@app-builder/utils/format';
4+
import { getRoute } from '@app-builder/utils/routes';
5+
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
6+
import { Link } from '@remix-run/react';
7+
import { MouseEventHandler, useState } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { Checkbox, cn } from 'ui-design-system';
10+
import { Icon } from 'ui-icons';
11+
import { CaseStatusBadge } from '../CaseStatus';
12+
import { AssignedContributors } from './AssignedContributors';
13+
import { PaginationRow, SuccessCasesQuery } from './PaginationRow';
14+
15+
export type CasesListProps = {
16+
casesQuery: SuccessCasesQuery;
17+
sorting: 'ASC' | 'DESC';
18+
onSortingChange: (sort: 'ASC' | 'DESC') => void;
19+
limit: number;
20+
setLimit: (limit: number) => void;
21+
isPaginationSticky: boolean;
22+
};
23+
24+
const handleRowClick: MouseEventHandler = (e) => {
25+
const rowLink = e.currentTarget.querySelector('[data-row-link]');
26+
if (rowLink && rowLink !== e.target && rowLink instanceof HTMLAnchorElement) {
27+
rowLink.dispatchEvent(new MouseEvent(e.type, e.nativeEvent));
28+
}
29+
};
30+
31+
export function CasesList({
32+
sorting,
33+
onSortingChange,
34+
casesQuery,
35+
limit,
36+
setLimit,
37+
isPaginationSticky,
38+
}: CasesListProps) {
39+
const {
40+
t,
41+
i18n: { language },
42+
} = useTranslation(['cases']);
43+
const [currentPage, setCurrentPage] = useState(0);
44+
const cases = casesQuery.data?.pages[currentPage]?.items ?? [];
45+
const { orgTags } = useOrganizationTags();
46+
47+
return (
48+
<div className="flex flex-col">
49+
<div className="w-full grid grid-cols-[0px_auto_1fr_auto_auto_auto_auto] border border-grey-border rounded-v2-md">
50+
<div className="grid grid-cols-subgrid col-span-full items-center group/table-row not-last:border-b border-grey-border">
51+
<HeaderCell className="ps-v2-xl relative col-span-2">
52+
<MultiSelect.Global>
53+
{(state, onSelect) => (
54+
<Checkbox
55+
checked={state}
56+
onClick={onSelect}
57+
className="absolute left-0 top-[50%] translate-[-50%] opacity-0 group-hover/table-row:opacity-100 data-[state=checked]:opacity-100 data-[state=indeterminate]:opacity-100"
58+
/>
59+
)}
60+
</MultiSelect.Global>
61+
Status
62+
</HeaderCell>
63+
<HeaderCell>Name of the case</HeaderCell>
64+
<HeaderCell>Review status</HeaderCell>
65+
<HeaderCell className="flex items-center gap-v2-sm justify-between">
66+
Date
67+
<Icon
68+
icon="caret-down"
69+
className={cn('size-5 cursor-pointer', {
70+
'rotate-180': sorting === 'ASC',
71+
})}
72+
onClick={() => onSortingChange(sorting === 'ASC' ? 'DESC' : 'ASC')}
73+
/>
74+
</HeaderCell>
75+
<HeaderCell>Tags</HeaderCell>
76+
<HeaderCell>
77+
<span className="hidden lg:inline">Assigned & Contributors</span>
78+
<span className="lg:hidden">Assign.</span>
79+
</HeaderCell>
80+
</div>
81+
{cases.map((caseItem) => (
82+
<div
83+
className="grid grid-cols-subgrid col-span-full items-center group/table-row hover:bg-purple-98 cursor-pointer"
84+
key={caseItem.id}
85+
onClick={handleRowClick}
86+
>
87+
<div className="invisible">
88+
<Link
89+
data-row-link
90+
to={getRoute('/cases/:caseId', { caseId: fromUUIDtoSUUID(caseItem.id) })}
91+
/>
92+
</div>
93+
<div className="relative p-v2-md ps-v2-xl w-25">
94+
<MultiSelect.Item id={caseItem.id}>
95+
{(isSelected, onSelect) => (
96+
<Checkbox
97+
// checked={selectedRows.includes(caseItem.id)}
98+
// data-row-id={caseItem.id}
99+
// onClick={handleCheckboxClick}
100+
checked={isSelected}
101+
onClick={onSelect}
102+
className="absolute left-0 top-[50%] translate-[-50%] opacity-0 group-hover/table-row:opacity-100 data-[state=checked]:opacity-100"
103+
/>
104+
)}
105+
</MultiSelect.Item>
106+
<CaseStatusBadge status={caseItem.status} size="large" showText={false} />
107+
</div>
108+
<div className="p-v2-md group-hover/table-row:text-purple-65 group-hover/table-row:underline">
109+
{caseItem.name}
110+
</div>
111+
<div className="p-v2-md">
112+
{caseItem.outcome && caseItem.outcome !== 'unset' ? (
113+
<span
114+
className={cn('rounded-full border px-v2-sm py-v2-xs text-small', {
115+
'border-red-47 text-red-47': caseItem.outcome === 'confirmed_risk',
116+
'border-green-38 text-green-38': caseItem.outcome === 'valuable_alert',
117+
'border-grey-50 text-grey-50': caseItem.outcome === 'false_positive',
118+
})}
119+
>
120+
{t(`cases:case.outcome.${caseItem.outcome}`)}
121+
</span>
122+
) : (
123+
'-'
124+
)}
125+
</div>
126+
<div className="p-v2-md">{formatDateRelative(caseItem.createdAt, { language })}</div>
127+
<div className="p-v2-md flex gap-v2-sm">
128+
{caseItem.tags.map((tagItem) => {
129+
const tag = orgTags.find((tag) => tag.id === tagItem.tagId);
130+
if (!tag) return null;
131+
return <TagPreview key={tag.id} name={tag.name} />;
132+
})}
133+
</div>
134+
<div className="p-v2-md">
135+
<AssignedContributors
136+
assignedTo={caseItem.assignedTo}
137+
contributors={caseItem.contributors}
138+
/>
139+
</div>
140+
</div>
141+
))}
142+
</div>
143+
<PaginationRow
144+
casesQuery={casesQuery}
145+
currentPage={currentPage}
146+
currentLimit={limit}
147+
setCurrentPage={setCurrentPage}
148+
setLimit={setLimit}
149+
className={isPaginationSticky ? 'shadow-sticky-bottom border-grey-border' : ''}
150+
/>
151+
</div>
152+
);
153+
}
154+
155+
const HeaderCell = ({ children, className }: { children: React.ReactNode; className?: string }) => {
156+
return (
157+
<div
158+
className={cn(
159+
'p-v2-md font-normal text-left not-first:border-l border-grey-border',
160+
className,
161+
)}
162+
>
163+
{children}
164+
</div>
165+
);
166+
};
167+
168+
const TagPreview = ({ name }: { name: string }) => (
169+
<div className="bg-purple-96 flex size-fit flex-row items-center gap-2 rounded-full px-2 py-[3px]">
170+
<span className="text-purple-65 text-xs font-normal">{name}</span>
171+
</div>
172+
);

0 commit comments

Comments
 (0)