Skip to content

Commit c3024d4

Browse files
authored
Merge pull request #496 from STAPLE-verse/fix-notification-visuals
Fix notification visuals
2 parents c66f64e + 8be855a commit c3024d4

File tree

13 files changed

+196
-137
lines changed

13 files changed

+196
-137
lines changed

db/middlewares/projectMemberMiddleware.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ export default function projectMemberMiddleware(prisma) {
66
params.model === "ProjectMember" &&
77
(params.action === "findMany" || params.action === "findFirst")
88
) {
9-
// Check if `deleted` is explicitly set to `undefined`
10-
const hasExplicitUndefined =
11-
"deleted" in params.args.where && params.args.where.deleted === undefined
9+
const hasExplicitDeleted = "deleted" in (params.args.where || {})
1210

13-
if (!hasExplicitUndefined) {
11+
if (!hasExplicitDeleted) {
1412
params.args.where = {
1513
...params.args.where,
16-
deleted: false, // ✅ Always filter out soft-deleted members
14+
deleted: false, // ✅ Always filter out soft-deleted members unless caller overrides
1715
}
1816
}
1917
}

db/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ model ProjectMember {
9696
tags Json?
9797
commentReadStatus CommentReadStatus[]
9898
notes Note[]
99+
formerTeamIds Json?
99100
}
100101

101102
model ProjectPrivilege {

src/contributors/components/ContributorForm.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function ContributorForm<S extends z.ZodType<any, any>>(props: Contributo
130130
/>
131131
)}
132132
<LabelSelectField
133-
className="select text-primary select-bordered border-primary border-2 w-1/2 mb-4 w-1/2"
133+
className="select text-primary bg-base-300 select-bordered bg-base-300 border-primary border-2 w-1/2 mb-4 w-1/2"
134134
name="privilege"
135135
label="Select Privilege:"
136136
options={MemberPrivilegesOptions}
@@ -164,6 +164,9 @@ export function ContributorForm<S extends z.ZodType<any, any>>(props: Contributo
164164
/>
165165
</span>
166166
</label>
167+
<p className="text-md italic text-base-content/80 mb-2">
168+
Tags only save after you press enter, comma, or semicolon.
169+
</p>
167170
<ReactTags
168171
tags={tags}
169172
name="tags"

src/contributors/mutations/deleteContributor.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ export default resolver.pipe(
2626
// Get the userId from the associated users array
2727
const userId = contributorToDelete.users[0]!.id
2828

29+
// Reconstruct possible display names used in notification messages
30+
const user = contributorToDelete.users[0]
31+
const possibleDisplayNames: string[] = []
32+
33+
if (user!.firstName && user!.lastName) {
34+
possibleDisplayNames.push(`${user!.firstName} ${user!.lastName}`)
35+
}
36+
37+
if (user!.username) {
38+
possibleDisplayNames.push(user!.username)
39+
}
40+
41+
const notificationMarkerText = " (former contributor)"
42+
const notificationMarkerHtml =
43+
'<span class="text-base-content/70" data-former-contributor="true"> (former contributor)</span>'
44+
2945
// Check if the project member has any privileges related to the project
3046
const projectPrivilege = await db.projectPrivilege.findFirst({
3147
where: {
@@ -75,6 +91,8 @@ export default resolver.pipe(
7591
},
7692
})
7793

94+
const formerTeamIds = teamProjectMembers.map((teamMember) => teamMember.id)
95+
7896
// Disconnect the user from each team project member individually
7997
for (const teamMember of teamProjectMembers) {
8098
await db.projectMember.update({
@@ -87,6 +105,72 @@ export default resolver.pipe(
87105
})
88106
}
89107

108+
// Annotate existing notifications that reference this contributor by name
109+
if (possibleDisplayNames.length > 0) {
110+
console.log(
111+
`[deleteContributor] Annotating notifications for user ${userId} with names:`,
112+
possibleDisplayNames
113+
)
114+
115+
const notifications = await db.notification.findMany({
116+
where: {
117+
projectId: contributorToDelete.projectId,
118+
announcement: false,
119+
AND: [
120+
{
121+
NOT: {
122+
message: {
123+
endsWith: notificationMarkerText,
124+
},
125+
},
126+
},
127+
{
128+
NOT: {
129+
message: {
130+
endsWith: notificationMarkerHtml,
131+
},
132+
},
133+
},
134+
{
135+
OR: possibleDisplayNames.map((name) => ({
136+
message: {
137+
contains: name,
138+
mode: "insensitive",
139+
},
140+
})),
141+
},
142+
],
143+
},
144+
select: {
145+
id: true,
146+
message: true,
147+
},
148+
})
149+
150+
console.log(
151+
`[deleteContributor] Found ${notifications.length} notifications requiring markers`
152+
)
153+
154+
await Promise.all(
155+
notifications.map((n) => {
156+
const trimmed = n.message!.trim()
157+
const containsHtml = /<\/?[a-z][\s\S]*>/i.test(trimmed)
158+
const marker = containsHtml ? notificationMarkerHtml : notificationMarkerText
159+
160+
return db.notification.update({
161+
where: { id: n.id },
162+
data: {
163+
message: `${trimmed}${marker}`,
164+
},
165+
})
166+
})
167+
)
168+
} else {
169+
console.log(
170+
`[deleteContributor] No display names detected for user ${userId}, skipping notification annotations`
171+
)
172+
}
173+
90174
// Disconnect the notifications related to the project
91175
const notificationsToUpdate = await db.notification.findMany({
92176
where: {
@@ -118,7 +202,10 @@ export default resolver.pipe(
118202
// Mark the project member as deleted
119203
const projectMember = await db.projectMember.update({
120204
where: { id: contributorToDelete.id },
121-
data: { deleted: true },
205+
data: {
206+
deleted: true,
207+
formerTeamIds: formerTeamIds.length > 0 ? formerTeamIds : undefined,
208+
},
122209
})
123210

124211
return projectMember

src/invites/mutations/acceptInvite.ts

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import sendNotification from "src/notifications/mutations/sendNotification"
55
import { getPrivilegeText } from "src/core/utils/getPrivilegeText"
66
import { Routes } from "@blitzjs/next"
77

8+
const parseFormerTeamIds = (value: unknown): number[] => {
9+
if (!Array.isArray(value)) return []
10+
return value
11+
.map((id) => {
12+
if (typeof id === "number") return id
13+
const parsed = Number(id)
14+
return Number.isFinite(parsed) ? parsed : null
15+
})
16+
.filter((id): id is number => id !== null)
17+
}
18+
819
export default resolver.pipe(
920
resolver.zod(AcceptInviteSchema),
1021
resolver.authorize(),
@@ -17,27 +28,86 @@ export default resolver.pipe(
1728
if (!invite) throw new Error("Invitation not found")
1829

1930
let projectMember
31+
let formerTeamIds: number[] = []
32+
33+
const reconnectFormerTeams = async (teamIds: number[]) => {
34+
if (teamIds.length === 0) return
35+
36+
for (const teamId of teamIds) {
37+
try {
38+
await db.projectMember.update({
39+
where: { id: teamId },
40+
data: {
41+
users: {
42+
connect: { id: userId },
43+
},
44+
},
45+
})
46+
} catch (error) {
47+
console.error(
48+
`[acceptInvite] Failed to reconnect user ${userId} to team ${teamId}:`,
49+
error
50+
)
51+
}
52+
}
53+
}
2054

2155
// Check if this is a reassignment invitation
2256
if (invite.reassignmentFor) {
57+
const reassignmentTarget = await db.projectMember.findUnique({
58+
where: { id: invite.reassignmentFor },
59+
})
60+
if (!reassignmentTarget) {
61+
throw new Error("Reassignment target not found")
62+
}
63+
formerTeamIds = parseFormerTeamIds(reassignmentTarget.formerTeamIds)
64+
2365
// Restore the soft-deleted ProjectMember
2466
projectMember = await db.projectMember.update({
2567
where: { id: invite.reassignmentFor },
26-
data: { deleted: false },
68+
data: {
69+
deleted: false,
70+
tags: invite.tags as any,
71+
formerTeamIds: formerTeamIds.length > 0 ? formerTeamIds : undefined,
72+
},
2773
})
2874
} else {
29-
// Create a new ProjectMember for fresh invitations
30-
projectMember = await db.projectMember.create({
31-
data: {
75+
// Check whether this user already has a soft-deleted ProjectMember for this project
76+
const existingProjectMember = await db.projectMember.findFirst({
77+
where: {
78+
projectId: invite.projectId,
3279
users: {
33-
connect: { id: userId },
80+
some: { id: userId },
3481
},
35-
projectId: invite.projectId,
36-
tags: invite.tags as any,
3782
},
3883
})
84+
85+
if (existingProjectMember) {
86+
formerTeamIds = parseFormerTeamIds(existingProjectMember.formerTeamIds)
87+
projectMember = await db.projectMember.update({
88+
where: { id: existingProjectMember.id },
89+
data: {
90+
deleted: false,
91+
tags: invite.tags as any,
92+
formerTeamIds: formerTeamIds.length > 0 ? formerTeamIds : undefined,
93+
},
94+
})
95+
} else {
96+
// Create a new ProjectMember for fresh invitations
97+
projectMember = await db.projectMember.create({
98+
data: {
99+
users: {
100+
connect: { id: userId },
101+
},
102+
projectId: invite.projectId,
103+
tags: invite.tags as any,
104+
},
105+
})
106+
}
39107
}
40108

109+
await reconnectFormerTeams(formerTeamIds)
110+
41111
// Create the project privilege
42112
const projectPrivilege = await db.projectPrivilege.create({
43113
data: {

src/milestones/components/MilestoneForm.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ export function MilestoneForm<S extends z.ZodType<any, any>>(props: MilestoneFor
204204
/>
205205
</span>
206206
</label>
207+
<p className="text-md italic text-base-content/80 mb-2">
208+
Tags only save after you press enter, comma, or semicolon.
209+
</p>
207210
<ReactTags
208211
tags={tags}
209212
name="tags"

src/notifications/tables/processing/processNotification.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export function processNotification(
2626
return notifications.map((notification) => {
2727
const cleanMessage = stripHtmlTags(notification.message || "")
2828
const type = determineNotificationType(cleanMessage)
29-
const isMarkdown = type === "Project"
29+
const containsHtml = /<\/?[a-z][\s\S]*>/i.test(notification.message || "")
30+
const isMarkdown = type === "Project" && !containsHtml
3031

3132
return {
3233
id: notification.id,

src/notifications/tables/processing/processProjectNotification.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export function processProjectNotification(
1919
): ProjectNotificationData[] {
2020
return notifications.map((notification) => {
2121
const cleanMessage = stripHtmlTags(notification.message || "")
22-
const isMarkdown = determineNotificationType(notification.message || "Other") === "Project"
22+
const containsHtml = /<\/?[a-z][\s\S]*>/i.test(notification.message || "")
23+
const type = determineNotificationType(notification.message || "Other")
24+
const isMarkdown = type === "Project" && !containsHtml
2325

2426
return {
2527
id: notification.id,
@@ -28,7 +30,7 @@ export function processProjectNotification(
2830
rawMessage: notification.message || "",
2931
notification: notification,
3032
routeData: notification.routeData as RouteData,
31-
type: determineNotificationType(notification.message || "Other"),
33+
type,
3234
isMarkdown,
3335
}
3436
})

src/projectmembers/components/ProjectMemberTaskList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,20 @@ const ProjectMemberTaskList = ({
4848
return () => eventBus.off("taskLogUpdated", handleTaskLogUpdate)
4949
}, [refetchTaskLogs])
5050

51+
const typedTaskLogs = taskLogs as TaskLogTaskCompleted[]
5152
const processedData = processTaskLogHistory(
52-
taskLogs as TaskLogTaskCompleted[],
53+
typedTaskLogs,
5354
comments,
5455
refetchComments,
5556
currentContributor,
5657
() => refetchTaskLogs()
5758
)
5859

60+
const hasTeamTasks = typedTaskLogs.some((log) => Boolean(log.assignedTo?.name))
61+
const cardTitle = hasTeamTasks ? "Team Tasks" : "Contributor Tasks"
62+
5963
return (
60-
<CollapseCard title="Contributor Tasks" className="mt-4">
64+
<CollapseCard title={cardTitle} className="mt-4">
6165
<Table columns={tableColumns} data={processedData} addPagination={true} />
6266
</CollapseCard>
6367
)

0 commit comments

Comments
 (0)