Skip to content

Commit b67758e

Browse files
committed
chore: added focus mode for task, improved logic and cleaning up
1 parent 3728ec8 commit b67758e

File tree

15 files changed

+1441
-431
lines changed

15 files changed

+1441
-431
lines changed

apps/api/src/task-management/dto/update-task-item.dto.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ApiPropertyOptional } from '@nestjs/swagger';
22
import { TaskItemPriority, TaskItemStatus } from "@db";
3-
import { IsEnum, IsOptional, IsString, ValidateIf } from "class-validator";
3+
import { IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from "class-validator";
44

55
export class UpdateTaskItemDto {
66
@ApiPropertyOptional({ description: 'Task title' })
77
@IsOptional()
88
@IsString()
9+
@IsNotEmpty()
910
title?: string;
1011

1112
@ApiPropertyOptional({ description: 'Task description' })

apps/api/src/task-management/task-item-assignment-notifier.service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ export class TaskItemAssignmentNotifierService {
9898
entityType,
9999
entityId,
100100
})}`;
101-
const taskUrl = taskUrlBase.includes('#')
102-
? taskUrlBase
103-
: `${taskUrlBase}#task-items`;
101+
// Deep-link directly to the TaskItems section + select the task
102+
const taskUrlObj = new URL(taskUrlBase);
103+
taskUrlObj.searchParams.set('taskItemId', taskItemId);
104+
taskUrlObj.hash = 'task-items';
105+
const taskUrl = taskUrlObj.toString();
104106

105107
const subscriberId = `${assigneeUser.id}-${organizationId}`;
106108
const workflowId = process.env.NOVU_WORKFLOW_TASK_ITEM_ASSIGNED ?? 'task-item-assigned';

apps/app/src/app/(app)/[orgId]/risk/[riskId]/page.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,49 @@ interface PageProps {
1919
sort?: string;
2020
page?: string;
2121
per_page?: string;
22+
taskItemId?: string;
2223
}>;
2324
params: Promise<{ riskId: string; orgId: string }>;
2425
}
2526

2627
export default async function RiskPage({ searchParams, params }: PageProps) {
2728
const { riskId, orgId } = await params;
29+
const { taskItemId } = await searchParams;
2830
const risk = await getRisk(riskId);
2931
const assignees = await getAssignees();
3032
if (!risk) {
3133
redirect('/');
3234
}
3335

36+
const shortTaskId = (id: string) => id.slice(-6).toUpperCase();
37+
3438
return (
3539
<PageWithBreadcrumb
3640
breadcrumbs={[
3741
{ label: 'Risks', href: `/${orgId}/risk` },
38-
{ label: risk.title, current: true },
42+
...(taskItemId
43+
? [
44+
{ label: risk.title, href: `/${orgId}/risk/${riskId}` },
45+
{ label: shortTaskId(taskItemId), current: true },
46+
]
47+
: [{ label: risk.title, current: true }]),
3948
]}
4049
headerRight={<RiskActions riskId={riskId} />}
4150
>
4251
<div className="flex flex-col gap-4">
43-
<RiskOverview risk={risk} assignees={assignees} />
44-
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
45-
<InherentRiskChart risk={risk} />
46-
<ResidualRiskChart risk={risk} />
47-
</div>
52+
{!taskItemId && (
53+
<>
54+
<RiskOverview risk={risk} assignees={assignees} />
55+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
56+
<InherentRiskChart risk={risk} />
57+
<ResidualRiskChart risk={risk} />
58+
</div>
59+
</>
60+
)}
4861
<TaskItems entityId={riskId} entityType="risk" />
49-
<Comments entityId={riskId} entityType={CommentEntityType.risk} />
62+
{!taskItemId && (
63+
<Comments entityId={riskId} entityType={CommentEntityType.risk} />
64+
)}
5065
</div>
5166
</PageWithBreadcrumb>
5267
);

apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/page.tsx

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,54 @@ import { SecondaryFields } from './components/secondary-fields/secondary-fields'
1616

1717
interface PageProps {
1818
params: Promise<{ vendorId: string; locale: string; orgId: string }>;
19+
searchParams?: Promise<{
20+
taskItemId?: string;
21+
}>;
1922
}
2023

21-
export default async function VendorPage({ params }: PageProps) {
24+
export default async function VendorPage({ params, searchParams }: PageProps) {
2225
const { vendorId, orgId } = await params;
26+
const { taskItemId } = (await searchParams) ?? {};
2327
const vendor = await getVendor(vendorId);
2428
const assignees = await getAssignees();
2529

2630
if (!vendor || !vendor.vendor) {
2731
redirect('/');
2832
}
2933

34+
const shortTaskId = (id: string) => id.slice(-6).toUpperCase();
35+
3036
return (
3137
<PageWithBreadcrumb
3238
breadcrumbs={[
3339
{ label: 'Vendors', href: `/${orgId}/vendors` },
34-
{ label: vendor.vendor?.name ?? '', current: true },
40+
...(taskItemId
41+
? [
42+
{ label: vendor.vendor?.name ?? '', href: `/${orgId}/vendors/${vendorId}` },
43+
{ label: shortTaskId(taskItemId), current: true },
44+
]
45+
: [{ label: vendor.vendor?.name ?? '', current: true }]),
3546
]}
3647
headerRight={<VendorActions vendorId={vendorId} />}
3748
>
3849
<div className="flex flex-col gap-4">
39-
<SecondaryFields
40-
vendor={vendor.vendor}
41-
assignees={assignees}
42-
globalVendor={vendor.globalVendor}
43-
/>
44-
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
45-
<VendorInherentRiskChart vendor={vendor.vendor} />
46-
<VendorResidualRiskChart vendor={vendor.vendor} />
47-
</div>
50+
{!taskItemId && (
51+
<>
52+
<SecondaryFields
53+
vendor={vendor.vendor}
54+
assignees={assignees}
55+
globalVendor={vendor.globalVendor}
56+
/>
57+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
58+
<VendorInherentRiskChart vendor={vendor.vendor} />
59+
<VendorResidualRiskChart vendor={vendor.vendor} />
60+
</div>
61+
</>
62+
)}
4863
<TaskItems entityId={vendorId} entityType="vendor" />
49-
<Comments entityId={vendorId} entityType={CommentEntityType.vendor} />
64+
{!taskItemId && (
65+
<Comments entityId={vendorId} entityType={CommentEntityType.vendor} />
66+
)}
5067
</div>
5168
</PageWithBreadcrumb>
5269
);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client';
2+
3+
import { Avatar, AvatarFallback, AvatarImage } from '@comp/ui/avatar';
4+
import { Circle } from 'lucide-react';
5+
import { formatDistanceToNow } from 'date-fns';
6+
import type { TaskItem } from '@/hooks/use-task-items';
7+
8+
interface TaskItemActivityTimelineProps {
9+
taskItem: TaskItem;
10+
}
11+
12+
export function TaskItemActivityTimeline({ taskItem }: TaskItemActivityTimelineProps) {
13+
return (
14+
<div className="space-y-4">
15+
<div className="flex items-center justify-between">
16+
<h3 className="text-sm font-semibold">Activity</h3>
17+
</div>
18+
<div className="relative">
19+
{/* Timeline line */}
20+
<div className="absolute left-3 top-6 bottom-0 w-px bg-border" />
21+
22+
<div className="space-y-4">
23+
{/* Created activity */}
24+
<div className="flex items-start gap-3 relative">
25+
<Avatar className="h-6 w-6 border border-border relative z-10 bg-background">
26+
<AvatarImage
27+
src={taskItem.createdBy.user.image || undefined}
28+
alt={taskItem.createdBy.user.name}
29+
/>
30+
<AvatarFallback className="text-[10px] bg-muted">
31+
{taskItem.createdBy.user.name?.charAt(0).toUpperCase() ?? '?'}
32+
</AvatarFallback>
33+
</Avatar>
34+
<div className="flex-1 min-w-0">
35+
<p className="text-sm">
36+
<span className="font-medium">{taskItem.createdBy.user.name}</span> created the
37+
task
38+
</p>
39+
<p className="text-xs text-muted-foreground">
40+
{formatDistanceToNow(new Date(taskItem.createdAt), { addSuffix: true })}
41+
</p>
42+
</div>
43+
</div>
44+
45+
{/* Status change activity (if updated) */}
46+
{taskItem.updatedBy && taskItem.updatedBy.id !== taskItem.createdBy.id && (
47+
<div className="flex items-start gap-3 relative">
48+
<div className="h-6 w-6 rounded-full bg-yellow-500/20 flex items-center justify-center relative z-10 border-2 border-background">
49+
<Circle className="h-3 w-3 text-yellow-500 fill-current" />
50+
</div>
51+
<div className="flex-1 min-w-0">
52+
<p className="text-sm">
53+
<span className="font-medium">{taskItem.updatedBy.user.name}</span> updated the
54+
task
55+
</p>
56+
<p className="text-xs text-muted-foreground">
57+
{formatDistanceToNow(new Date(taskItem.updatedAt), { addSuffix: true })}
58+
</p>
59+
</div>
60+
</div>
61+
)}
62+
63+
{/* Assignment activity (if assigned) */}
64+
{taskItem.assignee && (
65+
<div className="flex items-start gap-3 relative">
66+
<Avatar className="h-6 w-6 border border-border relative z-10 bg-background">
67+
<AvatarImage
68+
src={taskItem.assignee.user.image || undefined}
69+
alt={taskItem.assignee.user.name}
70+
/>
71+
<AvatarFallback className="text-[10px] bg-muted">
72+
{taskItem.assignee.user.name?.charAt(0).toUpperCase() ?? '?'}
73+
</AvatarFallback>
74+
</Avatar>
75+
<div className="flex-1 min-w-0">
76+
<p className="text-sm">
77+
Assigned to <span className="font-medium">{taskItem.assignee.user.name}</span>
78+
</p>
79+
<p className="text-xs text-muted-foreground">
80+
{taskItem.updatedBy && taskItem.updatedBy.id !== taskItem.createdBy.id
81+
? `by ${taskItem.updatedBy.user.name}`
82+
: `by ${taskItem.createdBy.user.name}`}
83+
</p>
84+
</div>
85+
</div>
86+
)}
87+
</div>
88+
</div>
89+
</div>
90+
);
91+
}
92+

0 commit comments

Comments
 (0)