Update configuration settings and improve notifications
This commit updates the configuration settings and improves the notification functionality by filtering out duplicate notifications. It also includes changes to feature flag data types and removed dependencies in `package.json`, alongside updates to `pnpm-lock.yaml`.
This commit is contained in:
@@ -15,11 +15,5 @@
|
|||||||
"@playwright/test": "^1.43.1",
|
"@playwright/test": "^1.43.1",
|
||||||
"@types/node": "^20.12.7",
|
"@types/node": "^20.12.7",
|
||||||
"node-html-parser": "^6.1.13"
|
"node-html-parser": "^6.1.13"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tanstack/react-table": "^8.16.0",
|
|
||||||
"next": "14.2.3",
|
|
||||||
"tailwind-merge": "^2.3.0",
|
|
||||||
"zod": "^3.23.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type AccountModel = {
|
|||||||
|
|
||||||
export function TeamAccountLayoutSidebar(props: {
|
export function TeamAccountLayoutSidebar(props: {
|
||||||
account: string;
|
account: string;
|
||||||
|
accountId: string;
|
||||||
accounts: AccountModel[];
|
accounts: AccountModel[];
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
@@ -40,6 +41,7 @@ export function TeamAccountLayoutSidebar(props: {
|
|||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
account={props.account}
|
account={props.account}
|
||||||
|
accountId={props.accountId}
|
||||||
accounts={props.accounts}
|
accounts={props.accounts}
|
||||||
user={props.user}
|
user={props.user}
|
||||||
/>
|
/>
|
||||||
@@ -50,6 +52,7 @@ export function TeamAccountLayoutSidebar(props: {
|
|||||||
|
|
||||||
function SidebarContainer(props: {
|
function SidebarContainer(props: {
|
||||||
account: string;
|
account: string;
|
||||||
|
accountId: string;
|
||||||
accounts: AccountModel[];
|
accounts: AccountModel[];
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
setCollapsed: (collapsed: boolean) => void;
|
setCollapsed: (collapsed: boolean) => void;
|
||||||
@@ -61,7 +64,9 @@ function SidebarContainer(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarContent className={'h-16 justify-center'}>
|
<SidebarContent className={'h-16 justify-center'}>
|
||||||
<div className={'flex max-w-full items-center space-x-2'}>
|
<div
|
||||||
|
className={'flex max-w-full items-center justify-between space-x-4'}
|
||||||
|
>
|
||||||
<TeamAccountAccountsSelector
|
<TeamAccountAccountsSelector
|
||||||
selectedAccount={account}
|
selectedAccount={account}
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
@@ -69,7 +74,7 @@ function SidebarContainer(props: {
|
|||||||
|
|
||||||
<TeamAccountNotifications
|
<TeamAccountNotifications
|
||||||
userId={props.user.id}
|
userId={props.user.id}
|
||||||
accountId={account}
|
accountId={props.accountId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ export function TeamAccountNavigationMenu(props: {
|
|||||||
|
|
||||||
<div className={'flex justify-end space-x-2.5'}>
|
<div className={'flex justify-end space-x-2.5'}>
|
||||||
<TeamAccountAccountsSelector
|
<TeamAccountAccountsSelector
|
||||||
selectedAccount={account.id}
|
selectedAccount={account.slug}
|
||||||
accounts={accounts.map((account) => ({
|
accounts={accounts.map((account) => ({
|
||||||
label: account.name,
|
label: account.name,
|
||||||
value: account.id,
|
value: account.slug,
|
||||||
image: account.picture_url,
|
image: account.picture_url,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function TeamWorkspaceLayout({
|
|||||||
<TeamAccountLayoutSidebar
|
<TeamAccountLayoutSidebar
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
account={params.account}
|
account={params.account}
|
||||||
|
accountId={data.account.id}
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
user={data.user}
|
user={data.user}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -42,18 +42,14 @@ const FeatureFlagsSchema = z.object({
|
|||||||
description: `If set to user, use the user's preferred language. If set to application, use the application's default language.`,
|
description: `If set to user, use the user's preferred language. If set to application, use the application's default language.`,
|
||||||
})
|
})
|
||||||
.default('application'),
|
.default('application'),
|
||||||
enableNotifications: z
|
enableNotifications: z.boolean({
|
||||||
.boolean({
|
description: 'Enable notifications functionality',
|
||||||
description: 'Enable notifications functionality',
|
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
||||||
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
}),
|
||||||
})
|
realtimeNotifications: z.boolean({
|
||||||
.default(true),
|
description: 'Enable realtime for the notifications functionality',
|
||||||
realtimeNotifications: z
|
required_error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
||||||
.boolean({
|
}),
|
||||||
description: 'Enable realtime for the notifications functionality',
|
|
||||||
required_error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
|
||||||
})
|
|
||||||
.default(true),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const featuresFlagConfig = FeatureFlagsSchema.parse({
|
const featuresFlagConfig = FeatureFlagsSchema.parse({
|
||||||
|
|||||||
@@ -43,5 +43,11 @@
|
|||||||
"turbo": "^1.13.3",
|
"turbo": "^1.13.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export function AccountSelector({
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:shadow-primary/10 group w-full min-w-0 max-w-full px-2',
|
'dark:shadow-primary/10 group w-auto min-w-0 max-w-fit px-2',
|
||||||
{
|
{
|
||||||
'justify-between': !collapsed,
|
'justify-start': !collapsed,
|
||||||
'justify-center': collapsed,
|
'justify-center': collapsed,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
@@ -124,14 +124,10 @@ export function AccountSelector({
|
|||||||
>
|
>
|
||||||
{(account) => (
|
{(account) => (
|
||||||
<span className={'flex max-w-full items-center space-x-2'}>
|
<span className={'flex max-w-full items-center space-x-2'}>
|
||||||
<Avatar
|
<Avatar className={'h-5 w-5'}>
|
||||||
className={
|
|
||||||
'group-hover:border-border h-6 w-6 border border-transparent'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AvatarImage src={account.image ?? undefined} />
|
<AvatarImage src={account.image ?? undefined} />
|
||||||
|
|
||||||
<AvatarFallback>
|
<AvatarFallback className={'group-hover:bg-background'}>
|
||||||
{account.label ? account.label[0] : ''}
|
{account.label ? account.label[0] : ''}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -147,7 +143,7 @@ export function AccountSelector({
|
|||||||
)}
|
)}
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<CaretSortIcon className="ml-1 h-4 w-4 shrink-0 opacity-50" />
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
@@ -188,7 +184,7 @@ export function AccountSelector({
|
|||||||
data-name={account.label}
|
data-name={account.label}
|
||||||
data-slug={account.value}
|
data-slug={account.value}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group flex justify-between transition-colors',
|
'group my-1 flex justify-between transition-colors',
|
||||||
{
|
{
|
||||||
['bg-muted']: value === account.value,
|
['bg-muted']: value === account.value,
|
||||||
},
|
},
|
||||||
@@ -205,19 +201,16 @@ export function AccountSelector({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
<Avatar
|
<Avatar className={'mr-2 h-5 w-5'}>
|
||||||
className={cn(
|
|
||||||
'mr-2 h-6 w-6 border border-transparent',
|
|
||||||
{
|
|
||||||
['border-border']: value === account.value,
|
|
||||||
['group-hover:border-border ']:
|
|
||||||
value !== account.value,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AvatarImage src={account.image ?? undefined} />
|
<AvatarImage src={account.image ?? undefined} />
|
||||||
|
|
||||||
<AvatarFallback>
|
<AvatarFallback
|
||||||
|
className={cn({
|
||||||
|
['bg-background']: value === account.value,
|
||||||
|
['group-hover:bg-background']:
|
||||||
|
value !== account.value,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{account.label ? account.label[0] : ''}
|
{account.label ? account.label[0] : ''}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -241,7 +234,7 @@ export function AccountSelector({
|
|||||||
<Button
|
<Button
|
||||||
data-test={'create-team-account-trigger'}
|
data-test={'create-team-account-trigger'}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-start"
|
className="w-full justify-start rounded-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCreatingAccount(true);
|
setIsCreatingAccount(true);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Bell, CircleAlert, Info, TriangleAlert, XIcon } from 'lucide-react';
|
import { Bell, CircleAlert, Info, TriangleAlert, XIcon } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -33,7 +33,15 @@ export function NotificationsPopover(params: {
|
|||||||
|
|
||||||
const onNotifications = useCallback(
|
const onNotifications = useCallback(
|
||||||
(notifications: PartialNotification[]) => {
|
(notifications: PartialNotification[]) => {
|
||||||
setNotifications((existing) => [...notifications, ...existing]);
|
setNotifications((existing) => {
|
||||||
|
const unique = new Set(existing.map((notification) => notification.id));
|
||||||
|
|
||||||
|
const notificationsFiltered = notifications.filter(
|
||||||
|
(notification) => !unique.has(notification.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...notificationsFiltered, ...existing];
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
@@ -108,6 +116,12 @@ export function NotificationsPopover(params: {
|
|||||||
return text.slice(0, 1).toUpperCase() + text.slice(1);
|
return text.slice(0, 1).toUpperCase() + text.slice(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setNotifications([]);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover modal open={open} onOpenChange={setOpen}>
|
<Popover modal open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
|
||||||
|
import { useNotificationsStream } from './use-notifications-stream';
|
||||||
|
|
||||||
type Notification = {
|
type Notification = {
|
||||||
id: number;
|
id: number;
|
||||||
body: string;
|
body: string;
|
||||||
@@ -22,41 +24,21 @@ export function useFetchNotifications({
|
|||||||
accountIds: string[];
|
accountIds: string[];
|
||||||
realtime: boolean;
|
realtime: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { data: notifications } = useFetchInitialNotifications({ accountIds });
|
const { data: initialNotifications } = useFetchInitialNotifications({
|
||||||
const client = useSupabase();
|
accountIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
useNotificationsStream({
|
||||||
|
onNotifications,
|
||||||
|
accountIds,
|
||||||
|
enabled: realtime,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let realtimeSubscription: { unsubscribe: () => void } | null = null;
|
if (initialNotifications) {
|
||||||
|
onNotifications(initialNotifications);
|
||||||
if (realtime) {
|
|
||||||
const channel = client.channel('notifications-channel');
|
|
||||||
|
|
||||||
realtimeSubscription = channel
|
|
||||||
.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: 'INSERT',
|
|
||||||
schema: 'public',
|
|
||||||
filter: `account_id=in.(${accountIds.join(', ')})`,
|
|
||||||
table: 'notifications',
|
|
||||||
},
|
|
||||||
(payload) => {
|
|
||||||
onNotifications([payload.new as Notification]);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
}
|
||||||
|
}, [initialNotifications, onNotifications]);
|
||||||
if (notifications) {
|
|
||||||
onNotifications(notifications);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (realtimeSubscription) {
|
|
||||||
realtimeSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [client, onNotifications, accountIds, realtime, notifications]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFetchInitialNotifications(props: { accountIds: string[] }) {
|
function useFetchInitialNotifications(props: { accountIds: string[] }) {
|
||||||
@@ -86,5 +68,6 @@ function useFetchInitialNotifications(props: { accountIds: string[] }) {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
|
||||||
|
type Notification = {
|
||||||
|
id: number;
|
||||||
|
body: string;
|
||||||
|
dismissed: boolean;
|
||||||
|
type: 'info' | 'warning' | 'error';
|
||||||
|
created_at: string;
|
||||||
|
link: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useNotificationsStream(params: {
|
||||||
|
onNotifications: (notifications: Notification[]) => void;
|
||||||
|
accountIds: string[];
|
||||||
|
enabled: boolean;
|
||||||
|
}) {
|
||||||
|
const client = useSupabase();
|
||||||
|
|
||||||
|
const { data: subscription } = useQuery({
|
||||||
|
enabled: params.enabled,
|
||||||
|
queryKey: ['realtime-notifications', ...params.accountIds],
|
||||||
|
queryFn: () => {
|
||||||
|
const channel = client.channel('notifications-channel');
|
||||||
|
|
||||||
|
return channel
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: 'INSERT',
|
||||||
|
schema: 'public',
|
||||||
|
filter: `account_id=in.(${params.accountIds.join(', ')})`,
|
||||||
|
table: 'notifications',
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
params.onNotifications([payload.new as Notification]);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
void subscription?.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [subscription]);
|
||||||
|
}
|
||||||
13415
pnpm-lock.yaml
generated
13415
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user