Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 32 additions & 18 deletions packages/app-builder/src/components/Cases/Inbox/CasesList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { MultiSelect } from '@app-builder/components/MultiSelect';
import { useOrganizationTags } from '@app-builder/services/organization/organization-tags';
import { formatDateRelative } from '@app-builder/utils/format';
import { formatDateRelative, formatDateTimeWithoutPresets } from '@app-builder/utils/format';
import { getRoute } from '@app-builder/utils/routes';
import { fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
import { Link } from '@remix-run/react';
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Checkbox, cn } from 'ui-design-system';
import { Checkbox, cn, Tooltip } from 'ui-design-system';
import { Icon } from 'ui-icons';
import { CaseStatusBadge } from '../CaseStatus';
import { AssignedContributors } from './AssignedContributors';
Expand Down Expand Up @@ -57,13 +57,7 @@ export function CasesList({
<div className="grid grid-cols-subgrid col-span-full items-center group/table-row not-last:border-b border-grey-border">
<HeaderCell className="ps-v2-xl relative col-span-2">
<MultiSelect.Global>
{(state, onSelect) => (
<Checkbox
checked={state}
onClick={onSelect}
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"
/>
)}
{(state, onSelect) => <SelectionCheckbox selectionState={state} onSelect={onSelect} />}
</MultiSelect.Global>
{t('cases:inbox.heading.status')}
</HeaderCell>
Expand Down Expand Up @@ -96,13 +90,7 @@ export function CasesList({
</div>
<div className="relative p-v2-md ps-v2-xl w-25">
<MultiSelect.Item index={index} id={caseItem.id} item={caseItem}>
{(isSelected, onSelect) => (
<Checkbox
checked={isSelected}
onClick={onSelect}
className="absolute left-0 top-[50%] translate-[-50%] opacity-0 group-hover/table-row:opacity-100 data-[state=checked]:opacity-100"
/>
)}
{(isSelected, onSelect) => <SelectionCheckbox selectionState={isSelected} onSelect={onSelect} />}
</MultiSelect.Item>
<CaseStatusBadge status={caseItem.status} size="large" showText={false} />
</div>
Expand All @@ -112,7 +100,7 @@ export function CasesList({
<div className="p-v2-md">
{caseItem.outcome && caseItem.outcome !== 'unset' ? (
<span
className={cn('rounded-full border px-v2-sm py-v2-xs text-small', {
className={cn('rounded-full border px-v2-sm py-v2-xs text-small text-nowrap', {
'border-red-47 text-red-47': caseItem.outcome === 'confirmed_risk',
'border-green-38 text-green-38': caseItem.outcome === 'valuable_alert',
'border-grey-50 text-grey-50': caseItem.outcome === 'false_positive',
Expand All @@ -124,7 +112,17 @@ export function CasesList({
'-'
)}
</div>
<div className="p-v2-md">{formatDateRelative(caseItem.createdAt, { language })}</div>
<div className="p-v2-md">
<Tooltip.Default
content={formatDateTimeWithoutPresets(caseItem.createdAt, {
language,
dateStyle: 'long',
timeStyle: 'short',
})}
>
<time dateTime={caseItem.createdAt}>{formatDateRelative(caseItem.createdAt, { language })}</time>
</Tooltip.Default>
</div>
<div className="p-v2-md flex gap-v2-sm">
{caseItem.tags.map((tagItem) => {
const tag = orgTags.find((tag) => tag.id === tagItem.tagId);
Expand Down Expand Up @@ -163,3 +161,19 @@ const TagPreview = ({ name }: { name: string }) => (
<span className="text-purple-65 text-xs font-normal">{name}</span>
</div>
);

type SelectionCheckboxProps = {
selectionState: boolean | 'indeterminate';
onSelect: MouseEventHandler;
};

const SelectionCheckbox = ({ selectionState, onSelect }: SelectionCheckboxProps) => {
return (
<div
className="group/checkbox-parent absolute left-0 top-[50%] translate-[-50%] p-v2-md opacity-0 group-hover/table-row:opacity-100 has-data-[state=checked]:opacity-100"
onClick={onSelect}
>
<Checkbox checked={selectionState} />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DEFAULT_CASE_PAGINATION_SIZE } from '@app-builder/repositories/CaseRepo
import { getRoute } from '@app-builder/utils/routes';
import { fromSUUIDtoUUID, fromUUIDtoSUUID } from '@app-builder/utils/short-uuid';
import { useLoaderData } from '@remix-run/react';
import { SerializeFrom } from '@remix-run/server-runtime/dist/single-fetch';
import { Namespace } from 'i18next';
import QueryString from 'qs';
import { useTranslation } from 'react-i18next';
Expand All @@ -27,11 +28,14 @@ export const handle = {
</BreadCrumbLink>
);
},
({ isLast }: BreadCrumbProps) => {
const { t } = useTranslation(['navigation']);
({ isLast, data }: BreadCrumbProps<SerializeFrom<typeof loader>>) => {
const { t } = useTranslation(['navigation', 'cases']);
const currentInboxName = data.currentInbox?.name ?? t('cases:inbox.my-inbox.link');
const currentInboxId = data.currentInbox ? fromUUIDtoSUUID(data.currentInbox.id) : MY_INBOX_ID;

return (
<BreadCrumbLink to={getRoute('/cases/inboxes')} isLast={isLast}>
{t('navigation:case_manager.cases')}
<BreadCrumbLink to={getRoute('/cases/inboxes/:inboxId', { inboxId: currentInboxId })} isLast={isLast}>
{currentInboxName}
</BreadCrumbLink>
);
},
Expand All @@ -47,10 +51,11 @@ const pageQueryStringSchema = z.object({
export const loader = createServerFn([authMiddleware], async function casesInboxesLoader({ request, params, context }) {
const { inbox: inboxRepository } = context.authInfo;
const inboxes = await inboxRepository.listInboxesWithCaseCount();
const inboxId = params['inboxId'];
const inboxIdParam = params['inboxId'];

invariant(inboxId, 'inboxId is required');
invariant(inboxIdParam, 'inboxId is required');

const inboxId = inboxIdParam === MY_INBOX_ID ? inboxIdParam : fromSUUIDtoUUID(inboxIdParam);
let inboxUsersIds: string[] = [];
let currentInbox = inboxes.find((inbox) => inbox.id === inboxId);
if (currentInbox) {
Expand All @@ -62,7 +67,8 @@ export const loader = createServerFn([authMiddleware], async function casesInbox
const parsedSearchParams = pageQueryStringSchema.parse(Object.fromEntries(searchParams));

return {
inboxId: inboxId === MY_INBOX_ID ? inboxId : fromSUUIDtoUUID(inboxId),
inboxId,
currentInbox,
inboxes,
inboxUsersIds,
query: parsedSearchParams.q,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { z } from 'zod/v4';

const paginationSchema = z.object({
limit: z.coerce.number().optional(),
cursorId: z.union([z.string(), z.coerce.number()]).optional(),
cursorId: z.string().optional(),
});

export async function loader({ request, params }: LoaderFunctionArgs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MY_INBOX_ID } from '@app-builder/constants/inboxes';
import { createServerFn, data } from '@app-builder/core/requests';
import { authMiddleware } from '@app-builder/middlewares/auth-middleware';
import { handleRedirectMiddleware } from '@app-builder/middlewares/handle-redirect-middleware';
import { caseStatuses } from '@app-builder/models/cases';
import { filtersSchema } from '@app-builder/queries/cases/get-cases';
import { badRequest } from '@app-builder/utils/http/http-responses';
import { parseQuerySafe } from '@app-builder/utils/input-validation';
Expand All @@ -22,12 +23,15 @@ export const loader = createServerFn(
throw badRequest('Invalid query');
}
const filterInboxIds = inboxId === MY_INBOX_ID ? undefined : [inboxId];
const assigneeIdFilter = parsedQuery.data.assignee ? { assigneeId: parsedQuery.data.assignee } : {};
const statusesFilter = parsedQuery.data.statuses ?? caseStatuses.filter((status) => status !== 'closed');

const cases = await caseRepository.listCases({
...parsedQuery.data,
...parsedPagination.data,
statuses: statusesFilter,
inboxIds: filterInboxIds,
...(filterInboxIds === undefined ? { assigneeId: user.actorIdentity.userId } : {}),
...(filterInboxIds === undefined ? { assigneeId: user.actorIdentity.userId } : assigneeIdFilter),
});

return data({ data: cases });
Expand Down
15 changes: 10 additions & 5 deletions packages/app-builder/src/utils/input-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,19 @@ export async function parseParams<Output>(params: Params, schema: ZodType<Output
/**
* Parse and validate a Request. Doesn't throw if validation fails.
*/
export async function parseQuerySafe<Output>(request: Request, schema: ZodType<Output, any, any>) {
type ParseQuerySafeResult<Output> =
| z.ZodSafeParseSuccess<Output>
| (z.ZodSafeParseError<Output> & { searchParams: Record<string, unknown> });

export async function parseQuerySafe<T extends ZodType>(
request: Request,
schema: T,
): Promise<ParseQuerySafeResult<z.output<T>>> {
const searchParams = inputFromUrl(request);
const result = await schema.safeParseAsync(searchParams);

if (!result.success) {
return {
...result,
searchParams,
};
return { ...result, searchParams };
}
return result;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-design-system/src/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type { CheckedState } from '@radix-ui/react-checkbox';
const checkbox = cva(
[
'flex shrink-0 items-center justify-center rounded-sm border outline-hidden',
'bg-grey-100 hover:bg-purple-98 enabled:radix-state-checked:border-none enabled:radix-state-checked:bg-purple-65',
'bg-grey-100 hover:bg-purple-98 group-hover/checkbox-parent:bg-purple-98 enabled:radix-state-checked:border-none enabled:radix-state-checked:bg-purple-65',
'disabled:bg-grey-90 disabled:border-grey-80 disabled:radix-state-checked:border disabled:radix-state-checked:bg-grey-90 disabled:cursor-not-allowed',
],
{
Expand Down