mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
refactor: enhance inbox functionality with type guards and API service integration
- Introduced type guards for metadata in InboxSidebar to ensure safe access and improve type safety. - Refactored the useInbox hook to utilize the new notifications API service for fetching notifications, enhancing validation and error handling. - Added new API request/response schemas for notifications, improving structure and clarity. - Updated logic for loading and marking notifications as read, ensuring consistent state management and user experience.
This commit is contained in:
parent
c98cfac49f
commit
00596f991d
4 changed files with 294 additions and 99 deletions
|
|
@ -43,7 +43,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
import type { InboxItem } from "@/hooks/use-inbox";
|
import type { InboxItem } from "@/hooks/use-inbox";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import type { ConnectorIndexingMetadata } from "@/contracts/types/inbox.types";
|
import {
|
||||||
|
type ConnectorIndexingMetadata,
|
||||||
|
type NewMentionMetadata,
|
||||||
|
isConnectorIndexingMetadata,
|
||||||
|
isNewMentionMetadata,
|
||||||
|
} from "@/contracts/types/inbox.types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -206,9 +211,9 @@ export function InboxSidebar({
|
||||||
statusItems
|
statusItems
|
||||||
.filter((item) => item.type === "connector_indexing")
|
.filter((item) => item.type === "connector_indexing")
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
const metadata = item.metadata as ConnectorIndexingMetadata;
|
// Use type guard for safe metadata access
|
||||||
if (metadata?.connector_type) {
|
if (isConnectorIndexingMetadata(item.metadata)) {
|
||||||
connectorTypes.add(metadata.connector_type);
|
connectorTypes.add(item.metadata.connector_type);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -234,8 +239,11 @@ export function InboxSidebar({
|
||||||
if (activeTab === "status" && selectedConnector) {
|
if (activeTab === "status" && selectedConnector) {
|
||||||
items = items.filter((item) => {
|
items = items.filter((item) => {
|
||||||
if (item.type === "connector_indexing") {
|
if (item.type === "connector_indexing") {
|
||||||
const metadata = item.metadata as ConnectorIndexingMetadata;
|
// Use type guard for safe metadata access
|
||||||
return metadata?.connector_type === selectedConnector;
|
if (isConnectorIndexingMetadata(item.metadata)) {
|
||||||
|
return item.metadata.connector_type === selectedConnector;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false; // Hide document_processing when a specific connector is selected
|
return false; // Hide document_processing when a specific connector is selected
|
||||||
});
|
});
|
||||||
|
|
@ -297,21 +305,20 @@ export function InboxSidebar({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === "new_mention") {
|
if (item.type === "new_mention") {
|
||||||
const metadata = item.metadata as {
|
// Use type guard for safe metadata access
|
||||||
thread_id?: number;
|
if (isNewMentionMetadata(item.metadata)) {
|
||||||
comment_id?: number;
|
const searchSpaceId = item.search_space_id;
|
||||||
};
|
const threadId = item.metadata.thread_id;
|
||||||
const searchSpaceId = item.search_space_id;
|
const commentId = item.metadata.comment_id;
|
||||||
const threadId = metadata?.thread_id;
|
|
||||||
const commentId = metadata?.comment_id;
|
|
||||||
|
|
||||||
if (searchSpaceId && threadId) {
|
if (searchSpaceId && threadId) {
|
||||||
const url = commentId
|
const url = commentId
|
||||||
? `/dashboard/${searchSpaceId}/new-chat/${threadId}?commentId=${commentId}`
|
? `/dashboard/${searchSpaceId}/new-chat/${threadId}?commentId=${commentId}`
|
||||||
: `/dashboard/${searchSpaceId}/new-chat/${threadId}`;
|
: `/dashboard/${searchSpaceId}/new-chat/${threadId}`;
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onCloseMobileSidebar?.();
|
onCloseMobileSidebar?.();
|
||||||
router.push(url);
|
router.push(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -348,27 +355,35 @@ export function InboxSidebar({
|
||||||
const getStatusIcon = (item: InboxItem) => {
|
const getStatusIcon = (item: InboxItem) => {
|
||||||
// For mentions, show the author's avatar with initials fallback
|
// For mentions, show the author's avatar with initials fallback
|
||||||
if (item.type === "new_mention") {
|
if (item.type === "new_mention") {
|
||||||
const metadata = item.metadata as {
|
// Use type guard for safe metadata access
|
||||||
author_name?: string;
|
if (isNewMentionMetadata(item.metadata)) {
|
||||||
author_avatar_url?: string | null;
|
const authorName = item.metadata.author_name;
|
||||||
author_email?: string;
|
const avatarUrl = item.metadata.author_avatar_url;
|
||||||
};
|
const authorEmail = item.metadata.author_email;
|
||||||
const authorName = metadata?.author_name;
|
|
||||||
const avatarUrl = metadata?.author_avatar_url;
|
|
||||||
const authorEmail = metadata?.author_email;
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
{avatarUrl && <AvatarImage src={avatarUrl} alt={authorName || "User"} />}
|
||||||
|
<AvatarFallback className="text-[10px] bg-primary/10 text-primary">
|
||||||
|
{getInitials(authorName, authorEmail)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Fallback for invalid metadata
|
||||||
return (
|
return (
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={authorName || "User"} />}
|
|
||||||
<AvatarFallback className="text-[10px] bg-primary/10 text-primary">
|
<AvatarFallback className="text-[10px] bg-primary/10 text-primary">
|
||||||
{getInitials(authorName, authorEmail)}
|
{getInitials(null, null)}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For status items (connector/document), show status icons
|
// For status items (connector/document), show status icons
|
||||||
const status = item.metadata?.status as string | undefined;
|
// Safely access status from metadata
|
||||||
|
const metadata = item.metadata as Record<string, unknown>;
|
||||||
|
const status = typeof metadata?.status === "string" ? metadata.status : undefined;
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "in_progress":
|
case "in_progress":
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,115 @@ export const newMentionInboxItem = inboxItem.extend({
|
||||||
metadata: newMentionMetadata,
|
metadata: newMentionMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// API Request/Response Schemas
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request schema for getting notifications
|
||||||
|
*/
|
||||||
|
export const getNotificationsRequest = z.object({
|
||||||
|
queryParams: z.object({
|
||||||
|
search_space_id: z.number().optional(),
|
||||||
|
type: inboxItemTypeEnum.optional(),
|
||||||
|
before_date: z.string().optional(),
|
||||||
|
limit: z.number().min(1).max(100).optional(),
|
||||||
|
offset: z.number().min(0).optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response schema for listing notifications
|
||||||
|
*/
|
||||||
|
export const getNotificationsResponse = z.object({
|
||||||
|
items: z.array(inboxItem),
|
||||||
|
total: z.number(),
|
||||||
|
has_more: z.boolean(),
|
||||||
|
next_offset: z.number().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request schema for marking a single notification as read
|
||||||
|
*/
|
||||||
|
export const markNotificationReadRequest = z.object({
|
||||||
|
notificationId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response schema for mark as read operations
|
||||||
|
*/
|
||||||
|
export const markNotificationReadResponse = z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response schema for mark all as read operation
|
||||||
|
*/
|
||||||
|
export const markAllNotificationsReadResponse = z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
message: z.string(),
|
||||||
|
updated_count: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Type Guards for Metadata
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for ConnectorIndexingMetadata
|
||||||
|
*/
|
||||||
|
export function isConnectorIndexingMetadata(
|
||||||
|
metadata: unknown
|
||||||
|
): metadata is ConnectorIndexingMetadata {
|
||||||
|
return connectorIndexingMetadata.safeParse(metadata).success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for DocumentProcessingMetadata
|
||||||
|
*/
|
||||||
|
export function isDocumentProcessingMetadata(
|
||||||
|
metadata: unknown
|
||||||
|
): metadata is DocumentProcessingMetadata {
|
||||||
|
return documentProcessingMetadata.safeParse(metadata).success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for NewMentionMetadata
|
||||||
|
*/
|
||||||
|
export function isNewMentionMetadata(metadata: unknown): metadata is NewMentionMetadata {
|
||||||
|
return newMentionMetadata.safeParse(metadata).success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe metadata parser - returns typed metadata or null
|
||||||
|
*/
|
||||||
|
export function parseInboxItemMetadata(
|
||||||
|
type: InboxItemTypeEnum,
|
||||||
|
metadata: unknown
|
||||||
|
): ConnectorIndexingMetadata | DocumentProcessingMetadata | NewMentionMetadata | null {
|
||||||
|
switch (type) {
|
||||||
|
case "connector_indexing": {
|
||||||
|
const result = connectorIndexingMetadata.safeParse(metadata);
|
||||||
|
return result.success ? result.data : null;
|
||||||
|
}
|
||||||
|
case "document_processing": {
|
||||||
|
const result = documentProcessingMetadata.safeParse(metadata);
|
||||||
|
return result.success ? result.data : null;
|
||||||
|
}
|
||||||
|
case "new_mention": {
|
||||||
|
const result = newMentionMetadata.safeParse(metadata);
|
||||||
|
return result.success ? result.data : null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
// Inferred types
|
// Inferred types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
export type InboxItemTypeEnum = z.infer<typeof inboxItemTypeEnum>;
|
export type InboxItemTypeEnum = z.infer<typeof inboxItemTypeEnum>;
|
||||||
export type InboxItemStatusEnum = z.infer<typeof inboxItemStatusEnum>;
|
export type InboxItemStatusEnum = z.infer<typeof inboxItemStatusEnum>;
|
||||||
export type DocumentProcessingStageEnum = z.infer<typeof documentProcessingStageEnum>;
|
export type DocumentProcessingStageEnum = z.infer<typeof documentProcessingStageEnum>;
|
||||||
|
|
@ -146,3 +254,10 @@ export type InboxItem = z.infer<typeof inboxItem>;
|
||||||
export type ConnectorIndexingInboxItem = z.infer<typeof connectorIndexingInboxItem>;
|
export type ConnectorIndexingInboxItem = z.infer<typeof connectorIndexingInboxItem>;
|
||||||
export type DocumentProcessingInboxItem = z.infer<typeof documentProcessingInboxItem>;
|
export type DocumentProcessingInboxItem = z.infer<typeof documentProcessingInboxItem>;
|
||||||
export type NewMentionInboxItem = z.infer<typeof newMentionInboxItem>;
|
export type NewMentionInboxItem = z.infer<typeof newMentionInboxItem>;
|
||||||
|
|
||||||
|
// API Request/Response types
|
||||||
|
export type GetNotificationsRequest = z.infer<typeof getNotificationsRequest>;
|
||||||
|
export type GetNotificationsResponse = z.infer<typeof getNotificationsResponse>;
|
||||||
|
export type MarkNotificationReadRequest = z.infer<typeof markNotificationReadRequest>;
|
||||||
|
export type MarkNotificationReadResponse = z.infer<typeof markNotificationReadResponse>;
|
||||||
|
export type MarkAllNotificationsReadResponse = z.infer<typeof markAllNotificationsReadResponse>;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { InboxItem, InboxItemTypeEnum } from "@/contracts/types/inbox.types";
|
import type { InboxItem, InboxItemTypeEnum } from "@/contracts/types/inbox.types";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { notificationsApiService } from "@/lib/apis/notifications-api.service";
|
||||||
import type { SyncHandle } from "@/lib/electric/client";
|
import type { SyncHandle } from "@/lib/electric/client";
|
||||||
import { useElectricClient } from "@/lib/electric/context";
|
import { useElectricClient } from "@/lib/electric/context";
|
||||||
|
|
||||||
|
|
@ -198,7 +198,7 @@ export function useInbox(
|
||||||
|
|
||||||
const db = client.db as any;
|
const db = client.db as any;
|
||||||
|
|
||||||
// Initial fetch from PGLite
|
// Initial fetch from PGLite - no validation needed, schema is enforced by Electric SQL sync
|
||||||
const result = await client.db.query<InboxItem>(query, params);
|
const result = await client.db.query<InboxItem>(query, params);
|
||||||
|
|
||||||
if (mounted && result.rows) {
|
if (mounted && result.rows) {
|
||||||
|
|
@ -213,30 +213,20 @@ export function useInbox(
|
||||||
"[useInbox] Electric returned 0 items, checking API for older notifications"
|
"[useInbox] Electric returned 0 items, checking API for older notifications"
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const apiParams = new URLSearchParams();
|
// Use the API service with proper Zod validation for API responses
|
||||||
if (searchSpaceId !== null) {
|
const data = await notificationsApiService.getNotifications({
|
||||||
apiParams.append("search_space_id", String(searchSpaceId));
|
queryParams: {
|
||||||
}
|
search_space_id: searchSpaceId ?? undefined,
|
||||||
if (typeFilter) {
|
type: typeFilter ?? undefined,
|
||||||
apiParams.append("type", typeFilter);
|
limit: PAGE_SIZE,
|
||||||
}
|
},
|
||||||
apiParams.append("limit", String(PAGE_SIZE));
|
});
|
||||||
|
|
||||||
const response = await authenticatedFetch(
|
if (mounted) {
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications?${apiParams.toString()}`
|
if (data.items.length > 0) {
|
||||||
);
|
setInboxItems(data.items);
|
||||||
|
|
||||||
if (response.ok && mounted) {
|
|
||||||
const data = await response.json();
|
|
||||||
const apiItems: InboxItem[] = data.items.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
metadata: item.metadata || {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (apiItems.length > 0) {
|
|
||||||
setInboxItems(apiItems);
|
|
||||||
}
|
}
|
||||||
setHasMore(data.has_more ?? apiItems.length === PAGE_SIZE);
|
setHasMore(data.has_more);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useInbox] API fallback failed:", err);
|
console.error("[useInbox] API fallback failed:", err);
|
||||||
|
|
@ -254,10 +244,12 @@ export function useInbox(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (liveQuery.subscribe) {
|
if (liveQuery.subscribe) {
|
||||||
|
// Live query data comes from PGlite - no validation needed
|
||||||
liveQuery.subscribe((result: { rows: InboxItem[] }) => {
|
liveQuery.subscribe((result: { rows: InboxItem[] }) => {
|
||||||
if (mounted && result.rows) {
|
if (mounted && result.rows) {
|
||||||
|
const liveItems = result.rows;
|
||||||
|
|
||||||
setInboxItems((prev) => {
|
setInboxItems((prev) => {
|
||||||
const liveItems = result.rows;
|
|
||||||
const liveItemIds = new Set(liveItems.map((item) => item.id));
|
const liveItemIds = new Set(liveItems.map((item) => item.id));
|
||||||
|
|
||||||
// FIXED: Keep ALL items not in live result (not just slice)
|
// FIXED: Keep ALL items not in live result (not just slice)
|
||||||
|
|
@ -305,43 +297,26 @@ export function useInbox(
|
||||||
const oldestItem = inboxItems.length > 0 ? inboxItems[inboxItems.length - 1] : null;
|
const oldestItem = inboxItems.length > 0 ? inboxItems[inboxItems.length - 1] : null;
|
||||||
const beforeDate = oldestItem ? toISOString(oldestItem.created_at) : null;
|
const beforeDate = oldestItem ? toISOString(oldestItem.created_at) : null;
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (searchSpaceId !== null) {
|
|
||||||
params.append("search_space_id", String(searchSpaceId));
|
|
||||||
}
|
|
||||||
if (typeFilter) {
|
|
||||||
params.append("type", typeFilter);
|
|
||||||
}
|
|
||||||
// Only add before_date if we have a cursor
|
|
||||||
// Without before_date, API returns newest items first
|
|
||||||
if (beforeDate) {
|
|
||||||
params.append("before_date", beforeDate);
|
|
||||||
}
|
|
||||||
params.append("limit", String(PAGE_SIZE));
|
|
||||||
|
|
||||||
console.log("[useInbox] Loading more, before:", beforeDate ?? "none (initial)");
|
console.log("[useInbox] Loading more, before:", beforeDate ?? "none (initial)");
|
||||||
|
|
||||||
const response = await authenticatedFetch(
|
// Use the API service with proper Zod validation
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications?${params.toString()}`
|
const data = await notificationsApiService.getNotifications({
|
||||||
);
|
queryParams: {
|
||||||
|
search_space_id: searchSpaceId ?? undefined,
|
||||||
|
type: typeFilter ?? undefined,
|
||||||
|
before_date: beforeDate ?? undefined,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (data.items.length > 0) {
|
||||||
throw new Error("Failed to fetch notifications");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const apiItems: InboxItem[] = data.items.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
metadata: item.metadata || {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (apiItems.length > 0) {
|
|
||||||
// Functional update ensures we always merge with latest state
|
// Functional update ensures we always merge with latest state
|
||||||
setInboxItems((prev) => deduplicateAndSort([...prev, ...apiItems]));
|
// Items are already validated by the API service
|
||||||
|
setInboxItems((prev) => deduplicateAndSort([...prev, ...data.items]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use API's has_more flag if available, otherwise check count
|
// Use API's has_more flag
|
||||||
setHasMore(data.has_more ?? apiItems.length === PAGE_SIZE);
|
setHasMore(data.has_more);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useInbox] Load more failed:", err);
|
console.error("[useInbox] Load more failed:", err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -357,12 +332,10 @@ export function useInbox(
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(
|
// Use the API service with proper Zod validation
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications/${itemId}/read`,
|
const result = await notificationsApiService.markAsRead({ notificationId: itemId });
|
||||||
{ method: "PATCH" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!result.success) {
|
||||||
// Rollback on error
|
// Rollback on error
|
||||||
setInboxItems((prev) =>
|
setInboxItems((prev) =>
|
||||||
prev.map((item) => (item.id === itemId ? { ...item, read: false } : item))
|
prev.map((item) => (item.id === itemId ? { ...item, read: false } : item))
|
||||||
|
|
@ -370,7 +343,7 @@ export function useInbox(
|
||||||
}
|
}
|
||||||
// If successful, Electric SQL will sync the change and live query will update
|
// If successful, Electric SQL will sync the change and live query will update
|
||||||
// This ensures eventual consistency even if optimistic update was wrong
|
// This ensures eventual consistency even if optimistic update was wrong
|
||||||
return response.ok;
|
return result.success;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to mark as read:", err);
|
console.error("Failed to mark as read:", err);
|
||||||
// Rollback on error
|
// Rollback on error
|
||||||
|
|
@ -387,17 +360,15 @@ export function useInbox(
|
||||||
setInboxItems((prev) => prev.map((item) => ({ ...item, read: true })));
|
setInboxItems((prev) => prev.map((item) => ({ ...item, read: true })));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(
|
// Use the API service with proper Zod validation
|
||||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/notifications/read-all`,
|
const result = await notificationsApiService.markAllAsRead();
|
||||||
{ method: "PATCH" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!result.success) {
|
||||||
console.error("Failed to mark all as read");
|
console.error("Failed to mark all as read");
|
||||||
// On error, let Electric SQL sync correct the state
|
// On error, let Electric SQL sync correct the state
|
||||||
}
|
}
|
||||||
// Electric SQL will sync and live query will ensure consistency
|
// Electric SQL will sync and live query will ensure consistency
|
||||||
return response.ok;
|
return result.success;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to mark all as read:", err);
|
console.error("Failed to mark all as read:", err);
|
||||||
// On error, let Electric SQL sync correct the state
|
// On error, let Electric SQL sync correct the state
|
||||||
|
|
|
||||||
94
surfsense_web/lib/apis/notifications-api.service.ts
Normal file
94
surfsense_web/lib/apis/notifications-api.service.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import {
|
||||||
|
type GetNotificationsRequest,
|
||||||
|
type GetNotificationsResponse,
|
||||||
|
type MarkAllNotificationsReadResponse,
|
||||||
|
type MarkNotificationReadRequest,
|
||||||
|
type MarkNotificationReadResponse,
|
||||||
|
getNotificationsRequest,
|
||||||
|
getNotificationsResponse,
|
||||||
|
markAllNotificationsReadResponse,
|
||||||
|
markNotificationReadRequest,
|
||||||
|
markNotificationReadResponse,
|
||||||
|
} from "@/contracts/types/inbox.types";
|
||||||
|
import { ValidationError } from "../error";
|
||||||
|
import { baseApiService } from "./base-api.service";
|
||||||
|
|
||||||
|
class NotificationsApiService {
|
||||||
|
/**
|
||||||
|
* Get notifications with pagination
|
||||||
|
*/
|
||||||
|
getNotifications = async (
|
||||||
|
request: GetNotificationsRequest
|
||||||
|
): Promise<GetNotificationsResponse> => {
|
||||||
|
const parsedRequest = getNotificationsRequest.safeParse(request);
|
||||||
|
|
||||||
|
if (!parsedRequest.success) {
|
||||||
|
console.error("Invalid request:", parsedRequest.error);
|
||||||
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||||
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { queryParams } = parsedRequest.data;
|
||||||
|
|
||||||
|
// Build query string from params
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (queryParams.search_space_id !== undefined) {
|
||||||
|
params.append("search_space_id", String(queryParams.search_space_id));
|
||||||
|
}
|
||||||
|
if (queryParams.type) {
|
||||||
|
params.append("type", queryParams.type);
|
||||||
|
}
|
||||||
|
if (queryParams.before_date) {
|
||||||
|
params.append("before_date", queryParams.before_date);
|
||||||
|
}
|
||||||
|
if (queryParams.limit !== undefined) {
|
||||||
|
params.append("limit", String(queryParams.limit));
|
||||||
|
}
|
||||||
|
if (queryParams.offset !== undefined) {
|
||||||
|
params.append("offset", String(queryParams.offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
|
||||||
|
return baseApiService.get(
|
||||||
|
`/api/v1/notifications${queryString ? `?${queryString}` : ""}`,
|
||||||
|
getNotificationsResponse
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a single notification as read
|
||||||
|
*/
|
||||||
|
markAsRead = async (
|
||||||
|
request: MarkNotificationReadRequest
|
||||||
|
): Promise<MarkNotificationReadResponse> => {
|
||||||
|
const parsedRequest = markNotificationReadRequest.safeParse(request);
|
||||||
|
|
||||||
|
if (!parsedRequest.success) {
|
||||||
|
console.error("Invalid request:", parsedRequest.error);
|
||||||
|
const errorMessage = parsedRequest.error.issues.map((issue) => issue.message).join(", ");
|
||||||
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { notificationId } = parsedRequest.data;
|
||||||
|
|
||||||
|
return baseApiService.patch(
|
||||||
|
`/api/v1/notifications/${notificationId}/read`,
|
||||||
|
markNotificationReadResponse
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all notifications as read
|
||||||
|
*/
|
||||||
|
markAllAsRead = async (): Promise<MarkAllNotificationsReadResponse> => {
|
||||||
|
return baseApiService.patch(
|
||||||
|
"/api/v1/notifications/read-all",
|
||||||
|
markAllNotificationsReadResponse
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationsApiService = new NotificationsApiService();
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue