fix(referencelines): and bring back the notification for worker updates but disallow duplication (#5005)
This commit is contained in:
1 parent
4643d6dc40
commit
98f8187dfe
21 files changed
+285
-65
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
||||
import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Dialog, DialogContent } from '../Dialog/Dialog';
|
||||
import { ScrollArea } from '../ScrollArea/ScrollArea';
|
||||
import { Button } from '../Button/Button';
|
||||
import { useNotification } from '../../contextProviders';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
@@ -112,7 +112,7 @@ interface ErrorBoundaryError extends Error {
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
interface DefaultFallbackProps {
|
||||
interface DefaultFallbackProps extends FallbackProps {
|
||||
error: ErrorBoundaryError;
|
||||
context: string;
|
||||
resetErrorBoundary: () => void;
|
||||
@@ -135,6 +135,8 @@ const DefaultFallback = ({
|
||||
}: DefaultFallbackProps) => {
|
||||
const { t } = useTranslation('ErrorBoundary');
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const { show } = useNotification();
|
||||
|
||||
const title = `${t('Something went wrong')}${!isProduction && ` ${t('in')} ${context}`}.`;
|
||||
const subtitle = t('Sorry, something went wrong there. Try again.');
|
||||
|
||||
@@ -143,20 +145,32 @@ const DefaultFallback = ({
|
||||
const copyErrorToClipboard = () => {
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code);
|
||||
toast.success(t('Error copied to clipboard'));
|
||||
show({
|
||||
title: t('Success'),
|
||||
message: t('Error copied to clipboard'),
|
||||
type: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
toast.error(title, {
|
||||
description: subtitle,
|
||||
// Use a stable ID based on error message to support deduplication
|
||||
const errorId = `error-${errorTitle || error.message}`;
|
||||
|
||||
// We don't need to track shown state - instead rely on the notification deduplication system
|
||||
show({
|
||||
title,
|
||||
message: subtitle,
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
id: errorId,
|
||||
action: {
|
||||
label: t('Show Details'),
|
||||
onClick: () => setShowDetails(true),
|
||||
},
|
||||
duration: 0,
|
||||
});
|
||||
}, [error, subtitle, t, title]);
|
||||
}, [error, errorTitle, subtitle, t, title, show]);
|
||||
|
||||
if (isProduction) {
|
||||
return null;
|
||||
@@ -286,9 +300,8 @@ const ErrorBoundary = ({
|
||||
<ReactErrorBoundary
|
||||
fallbackRender={props => (
|
||||
<FallbackComponent
|
||||
error={props.error}
|
||||
{...props}
|
||||
context={context}
|
||||
resetErrorBoundary={props.resetErrorBoundary}
|
||||
/>
|
||||
)}
|
||||
onReset={onResetHandler}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useCallback, useEffect, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useCallback, useEffect, ReactNode, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Toaster, toast } from '../components';
|
||||
|
||||
@@ -6,7 +6,23 @@ const NotificationContext = createContext(null);
|
||||
|
||||
export const useNotification = () => useContext(NotificationContext);
|
||||
|
||||
const NotificationProvider = ({ children, service }) => {
|
||||
// Notification deduplication cache
|
||||
interface NotificationCacheEntry {
|
||||
timestamp: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface NotificationProviderProps {
|
||||
children: ReactNode;
|
||||
service?: any;
|
||||
deduplicationInterval?: number;
|
||||
}
|
||||
|
||||
const NotificationProvider = ({
|
||||
children,
|
||||
service,
|
||||
deduplicationInterval = 10000, // Default to 10 seconds
|
||||
}: NotificationProviderProps) => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
title: '',
|
||||
message: '',
|
||||
@@ -15,39 +31,138 @@ const NotificationProvider = ({ children, service }) => {
|
||||
type: 'info', // info, success, error
|
||||
};
|
||||
|
||||
// Cache for recent notifications to prevent duplicates
|
||||
// Structure: { [title_message_type]: { timestamp, id } }
|
||||
const recentNotificationsRef = useRef<Record<string, NotificationCacheEntry>>({});
|
||||
|
||||
// Use the configurable deduplication interval from props
|
||||
|
||||
const show = useCallback(options => {
|
||||
const { title, message, duration, position, type, promise } = {
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
duration,
|
||||
position,
|
||||
type,
|
||||
promise,
|
||||
allowDuplicates = false,
|
||||
deduplicationInterval: optionsDeduplicationInterval,
|
||||
action,
|
||||
} = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
|
||||
// Use the provider's deduplicationInterval by default, but allow it to be overridden per notification
|
||||
const notificationDeduplicationInterval = optionsDeduplicationInterval || deduplicationInterval;
|
||||
|
||||
if (promise) {
|
||||
return toast.promise(promise, {
|
||||
loading: title || 'Loading...',
|
||||
success: (data: unknown) => ({
|
||||
title: title || 'Success',
|
||||
description: typeof message === 'function' ? message(data) : message,
|
||||
}),
|
||||
error: (err: unknown) => ({
|
||||
title: title || 'Error',
|
||||
description: typeof message === 'function' ? message(err) : message,
|
||||
}),
|
||||
success: (data: unknown) => {
|
||||
const description = typeof message === 'function' ? message(data) : message;
|
||||
return {
|
||||
title: title || 'Success',
|
||||
description,
|
||||
};
|
||||
},
|
||||
error: (err: unknown) => {
|
||||
const description = typeof message === 'function' ? message(err) : message;
|
||||
return {
|
||||
title: title || 'Error',
|
||||
description,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return toast[type](title, {
|
||||
// Create a cache key from notification properties
|
||||
const messageStr = typeof message === 'function' ? 'function' : message;
|
||||
const cacheKey = `${title}_${messageStr}_${type}`;
|
||||
|
||||
// Handle deduplication
|
||||
if (!allowDuplicates && type === 'error') {
|
||||
const now = Date.now();
|
||||
const cachedNotification = recentNotificationsRef.current[cacheKey];
|
||||
|
||||
// First check if we've shown this notification recently
|
||||
if (cachedNotification) {
|
||||
const timeSinceLastShown = now - cachedNotification.timestamp;
|
||||
|
||||
// If it's been shown recently and within the deduplication interval,
|
||||
// don't show it again
|
||||
if (timeSinceLastShown < notificationDeduplicationInterval) {
|
||||
console.debug(
|
||||
`Prevented duplicate notification: "${title}" (${timeSinceLastShown}ms < ${notificationDeduplicationInterval}ms)`
|
||||
);
|
||||
// Return the existing notification ID
|
||||
return cachedNotification.id;
|
||||
}
|
||||
|
||||
// If it's been shown before but outside the deduplication interval,
|
||||
// dismiss the old notification first and allow showing a new one
|
||||
console.debug(
|
||||
`Showing notification again after interval: "${title}" (${timeSinceLastShown}ms >= ${notificationDeduplicationInterval}ms)`
|
||||
);
|
||||
toast.dismiss(cachedNotification.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Show the notification with action if provided
|
||||
const toastOptions = {
|
||||
duration,
|
||||
position,
|
||||
description: message,
|
||||
});
|
||||
id: options.id, // Use provided ID if available
|
||||
};
|
||||
|
||||
// Add action button if provided
|
||||
if (action && action.label && typeof action.onClick === 'function') {
|
||||
toastOptions.action = {
|
||||
label: action.label,
|
||||
onClick: action.onClick,
|
||||
};
|
||||
}
|
||||
|
||||
const id = toast[type](title, toastOptions);
|
||||
|
||||
// Cache this notification for deduplication if it's an error
|
||||
if (type === 'error') {
|
||||
const timestamp = Date.now();
|
||||
recentNotificationsRef.current[cacheKey] = {
|
||||
timestamp,
|
||||
id,
|
||||
};
|
||||
|
||||
console.debug(
|
||||
`Stored notification in cache: "${title}" (id: ${id}, timestamp: ${timestamp})`
|
||||
);
|
||||
|
||||
// We no longer auto-delete the cache entry after duration
|
||||
// Instead, we keep it to track when the notification was last shown
|
||||
// The entry will be checked against the deduplication interval
|
||||
}
|
||||
|
||||
return id;
|
||||
}, []);
|
||||
|
||||
const hide = useCallback(id => {
|
||||
toast.dismiss(id);
|
||||
|
||||
// Remove from cache if present
|
||||
const cacheEntries = Object.entries(recentNotificationsRef.current);
|
||||
for (const [key, entry] of cacheEntries) {
|
||||
if (entry.id === id) {
|
||||
delete recentNotificationsRef.current[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hideAll = useCallback(() => {
|
||||
toast.dismiss();
|
||||
// Clear notification cache
|
||||
recentNotificationsRef.current = {};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@@ -61,8 +176,28 @@ const NotificationProvider = ({ children, service }) => {
|
||||
}
|
||||
}, [service, hide, show]);
|
||||
|
||||
// Debug function to get the current cache (for development use)
|
||||
const getNotificationCache = useCallback(() => {
|
||||
const cache = { ...recentNotificationsRef.current };
|
||||
|
||||
// Add human-readable timestamps and time since showing
|
||||
const now = Date.now();
|
||||
const enhancedCache = Object.entries(cache).reduce((result, [key, entry]) => {
|
||||
const timeSince = now - entry.timestamp;
|
||||
result[key] = {
|
||||
...entry,
|
||||
date: new Date(entry.timestamp).toISOString(),
|
||||
timeSinceMs: timeSince,
|
||||
timeSinceStr: `${Math.floor(timeSince / 1000)}s ago`,
|
||||
};
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return enhancedCache;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={{ show, hide, hideAll }}>
|
||||
<NotificationContext.Provider value={{ show, hide, hideAll, getNotificationCache }}>
|
||||
<Toaster position="bottom-right" />
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
@@ -71,6 +206,8 @@ const NotificationProvider = ({ children, service }) => {
|
||||
|
||||
NotificationProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
service: PropTypes.object,
|
||||
deduplicationInterval: PropTypes.number,
|
||||
};
|
||||
|
||||
export const withNotification = Component => {
|
||||
|
||||
Reference in new issue
Block a user