Skip to content

Commit ee6e973

Browse files
authored
Send email when an issue is escalating (#1960)
We want to let users know when an issue is escalating. To do that we'll send an email to all the members of the workspaces that have activated the notifications for escalating issues.
1 parent d75e63b commit ee6e973

File tree

120 files changed

+12021
-1159
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+12021
-1159
lines changed

apps/web/public/latitude-logo.png

27.7 KB
Loading
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use server'
2+
3+
import { MembershipsRepository } from '@latitude-data/core/repositories'
4+
import { updateEscalatingIssuesEmailPreference } from '@latitude-data/core/services/memberships/updateEscalatingIssuesEmailPreference'
5+
import { z } from 'zod'
6+
7+
import { authProcedure } from '../procedures'
8+
9+
export const updateEscalatingIssuesEmailPreferenceAction = authProcedure
10+
.inputSchema(z.object({ wantToReceive: z.boolean() }))
11+
.action(
12+
async ({ parsedInput: { wantToReceive }, ctx: { workspace, user } }) => {
13+
const membershipsScope = new MembershipsRepository(workspace.id)
14+
const membership = await membershipsScope
15+
.findByUserId(user.id)
16+
.then((r) => r.unwrap())
17+
18+
return await updateEscalatingIssuesEmailPreference({
19+
membership,
20+
wantToReceive,
21+
userEmail: user.email,
22+
}).then((r) => r.unwrap())
23+
},
24+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use server'
2+
3+
import { MembershipsRepository } from '@latitude-data/core/repositories'
4+
import { updateWeeklyEmailPreference } from '@latitude-data/core/services/memberships/updateWeeklyEmailPreference'
5+
import { z } from 'zod'
6+
7+
import { authProcedure } from '../procedures'
8+
9+
export const updateWeeklyEmailPreferenceAction = authProcedure
10+
.inputSchema(z.object({ wantToReceive: z.boolean() }))
11+
.action(
12+
async ({ parsedInput: { wantToReceive }, ctx: { workspace, user } }) => {
13+
const membershipsScope = new MembershipsRepository(workspace.id)
14+
const membership = await membershipsScope
15+
.findByUserId(user.id)
16+
.then((r) => r.unwrap())
17+
18+
return await updateWeeklyEmailPreference({
19+
membership,
20+
wantToReceive,
21+
userEmail: user.email,
22+
}).then((r) => r.unwrap())
23+
},
24+
)

apps/web/src/actions/workspaces/switch.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { setSession } from '$/services/auth/setSession'
77
import { authProcedure } from '../procedures'
88
import { cookies } from 'next/headers'
99
import { removeSession, Session } from '$/services/auth/removeSession'
10+
import { redirect } from 'next/navigation'
1011

1112
export const switchWorkspaceAction = authProcedure
1213
.inputSchema(
1314
z.object({
1415
workspaceId: z.number(),
16+
redirectTo: z.string().optional(),
1517
}),
1618
)
1719
.action(async ({ parsedInput, ctx }) => {
@@ -37,5 +39,9 @@ export const switchWorkspaceAction = authProcedure
3739
await cookies(),
3840
)
3941

42+
if (parsedInput.redirectTo) {
43+
return redirect(parsedInput.redirectTo)
44+
}
45+
4046
return workspace
4147
})

apps/web/src/app/(admin)/backoffice/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { ReactNode } from 'react'
22

33
import buildMetatags from '$/app/_lib/buildMetatags'
44
import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
5-
import { SessionProvider } from '@latitude-data/web-ui/providers'
65
import { notFound } from 'next/navigation'
76

87
import { BackofficeTabs } from './_components/BackofficeTabs'
8+
import { SessionProvider } from '$/components/Providers/SessionProvider'
99

1010
export const metadata = buildMetatags({
1111
title: 'Backoffice',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Metadata } from 'next'
2+
import buildMetatags from '$/app/_lib/buildMetatags'
3+
import NotificationsModal from '$/components/Notifications/Modal'
4+
5+
export const metadata: Promise<Metadata> = buildMetatags({
6+
title: 'Notifications',
7+
locationDescription: 'Email Notifications',
8+
})
9+
10+
export default function NotificationsModalPage() {
11+
return <NotificationsModal />
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Default() {
2+
return null
3+
}

apps/web/src/app/(private)/dashboard/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ReactNode } from 'react'
2+
import { Metadata } from 'next'
23

34
import { Container } from '@latitude-data/web-ui/atoms/Container'
45
import { TableBlankSlate } from '@latitude-data/web-ui/molecules/TableBlankSlate'
@@ -12,15 +13,17 @@ import Link from 'next/link'
1213
import { getActiveProjectsCached } from '../_data-access'
1314
import { ProjectsTable } from './_components/ProjectsTable'
1415

15-
export const metadata = buildMetatags({
16+
export const metadata: Promise<Metadata> = buildMetatags({
1617
title: 'Dashboard',
1718
locationDescription: 'Projects List',
1819
})
1920

2021
export default async function DashboardLayout({
2122
children,
23+
modal,
2224
}: Readonly<{
2325
children: ReactNode
26+
modal: ReactNode
2427
}>) {
2528
const { workspace } = await getCurrentUserOrRedirect()
2629

@@ -30,6 +33,7 @@ export default async function DashboardLayout({
3033
<Container>
3134
<AppTabs />
3235
{children}
36+
{modal}
3337
<TableWithHeader
3438
title='Projects'
3539
actions={
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use client'
2+
3+
import { ROUTES } from '$/services/routes'
4+
import { Text } from '@latitude-data/web-ui/atoms/Text'
5+
import { useOnce } from '$/hooks/useMount'
6+
import { switchWorkspaceAction } from '$/actions/workspaces/switch'
7+
import useLatitudeAction from '$/hooks/useLatitudeAction'
8+
import { useToast } from '@latitude-data/web-ui/atoms/Toast'
9+
10+
/**
11+
* We do this here because next.js page can't change server cookies on the fly
12+
* so we need to have a client component that calls the action to switch workspace
13+
* and then redirect to the notifications page.
14+
*/
15+
export function WorkspaceSwitcherRedirect({
16+
targetWorkspaceId,
17+
}: {
18+
targetWorkspaceId: number
19+
}) {
20+
const { toast } = useToast()
21+
const { execute: switchWorkspace, isPending: isSwitching } =
22+
useLatitudeAction(switchWorkspaceAction)
23+
24+
useOnce(() => {
25+
async function performSwitch() {
26+
const [, error] = await switchWorkspace({
27+
workspaceId: targetWorkspaceId,
28+
redirectTo: ROUTES.dashboard.notifications.root,
29+
})
30+
31+
if (error) {
32+
toast({
33+
title: 'Failed to switch workspace',
34+
description: error.message,
35+
variant: 'destructive',
36+
})
37+
38+
return
39+
}
40+
}
41+
42+
performSwitch()
43+
})
44+
45+
return (
46+
<div className='flex items-center justify-center min-h-screen'>
47+
<div className='flex flex-col items-center gap-4'>
48+
<Text.H4>
49+
{isSwitching
50+
? 'Switching workspace...'
51+
: 'Redirecting to notifications...'}
52+
</Text.H4>
53+
</div>
54+
</div>
55+
)
56+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { notFound, redirect } from 'next/navigation'
2+
import { getCurrentUserOrRedirect } from '$/services/auth/getCurrentUser'
3+
import { WorkspaceSwitcherRedirect } from './_components/WorkspaceSwitcherRedirect'
4+
import { ROUTES } from '$/services/routes'
5+
import { WorkspacesRepository } from '@latitude-data/core/repositories'
6+
import { Result } from '@latitude-data/core/lib/Result'
7+
8+
/**
9+
* Handle notifications page by workspace id
10+
*
11+
* 1. If this workspace is valid we find if the user in the current session
12+
* (if any) has a membership in that workspace. If not current session we
13+
* redirect to login page with `redirectTo` param set to this page.
14+
*
15+
* 2. If the user has a membership in that workspace but is not the current
16+
* workspace we render workspace switcher redirect component to switch to that
17+
* so frontend can set the right session in cookies.
18+
*
19+
* 3. If the user has membership to that workspace and it's the current workspace
20+
* we redirect to the default notifications page
21+
*
22+
*/
23+
export default async function NotificationsByWorkspacePage({
24+
params,
25+
}: {
26+
params: Promise<{ workspaceId: string }>
27+
}) {
28+
const { user: currentUser, workspace: currentWorkspace } =
29+
await getCurrentUserOrRedirect()
30+
31+
const paramsResolved = await params
32+
const workspaceId = parseInt(paramsResolved.workspaceId, 10)
33+
const workspaceRepo = new WorkspacesRepository(currentUser.id)
34+
const workspaceResult = await workspaceRepo.find(workspaceId)
35+
36+
if (!Result.isOk(workspaceResult)) return notFound()
37+
38+
const workspace = workspaceResult.value
39+
if (workspace.id === currentWorkspace.id) {
40+
return redirect(ROUTES.dashboard.notifications.root)
41+
}
42+
43+
return <WorkspaceSwitcherRedirect targetWorkspaceId={workspace.id} />
44+
}

0 commit comments

Comments
 (0)