From 62aadc84cbbac26e1909dd2a88ec826b22d05524 Mon Sep 17 00:00:00 2001 From: Joep31 Date: Mon, 4 May 2026 21:05:53 +0200 Subject: [PATCH] add: projectId and enhanced error handling --- README.md | 72 +++++++++++++++---- package.json | 2 +- .../features/comments/comment-wrapper.tsx | 1 + playground/features/comments/server.ts | 6 +- src/components/comment-overlay.tsx | 18 ++++- src/contexts/comment-context.tsx | 13 ++++ src/errors.ts | 6 ++ src/types/comment.types.ts | 7 ++ src/types/error.types.ts | 3 +- src/types/overlay.types.ts | 22 +++++- 10 files changed, 127 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 79289a8..2444e51 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ import { } from '@kendew-agency/react-feedback-layer'; function App() { - const handleConfirm = async (comments) => { + const handleConfirm = async (comments, projectId) => { // Save comments to your database - await saveToDatabase(comments); + await saveToDatabase(comments, projectId); }; const handleResolve = async (comments) => { @@ -50,11 +50,16 @@ function App() { await resolveInDatabase(comments); }; + const handleError = (error) => { + console.error('Comment error:', error.code, error.message); + }; + return ( @@ -132,13 +137,13 @@ interface CommentOverlayProps { }; // Callback when user confirms draft comments - onConfirm: (comments: ConfirmedComment[]) => Promise; + onConfirm: (comments: ConfirmedComment[], projectId?: string) => Promise; // Callback when user resolves comments onResolve: (comments: ConfirmedComment[]) => Promise; - // Callback to handle errors (optional) - onError?: (error: KnownError) => void; + // Callback to handle errors + onError: (error: KnownError) => void; // Optional configuration for the comment layer config?: { @@ -152,6 +157,9 @@ interface CommentOverlayProps { hideResolving?: boolean; }; }; + + // Optional project ID to attach to all comments in this overlay + projectId?: string; } ``` @@ -161,8 +169,9 @@ interface CommentOverlayProps { { - await api.createComments(comments); + projectId="proj_123456789" + onConfirm={async (comments, projectId) => { + await api.createComments(comments, projectId); }} onResolve={async (comments) => { await api.resolveComments(comments); @@ -472,6 +481,7 @@ interface CommentContext { confirmComments: () => Promise<{ error: Error | DOMException | null }>; resolveComments: () => Promise<{ error: Error | DOMException | null }>; updateCommentVisibility: (visibility: Partial) => void; + triggerError: (error: KnownError) => void; } ``` @@ -570,6 +580,7 @@ type CommentType = { resolvedAt?: Date; status: "draft" | "published" | "resolving" | "resolved"; indicator?: Indicator | null; + projectId?: string; }; ``` @@ -675,10 +686,10 @@ function App() { return ( { + onConfirm={async (comments, projectId) => { const response = await fetch('/api/comments', { method: 'POST', - body: JSON.stringify(comments), + body: JSON.stringify({ comments, projectId }), }); if (!response.ok) { throw new Error('Failed to save comments'); @@ -749,6 +760,7 @@ function App() { subscription={subscription} onConfirm={saveComments} onResolve={resolveComments} + onError={(error) => console.error(error)} > {/* ... */} @@ -919,6 +931,27 @@ function VisibilityControls() { ## Advanced Patterns +### Project ID + +Attach a project ID to all comments created within an overlay. This is useful for multi-project setups where you need to associate comments with a specific project in your database. + +```tsx + { + // projectId is passed through to your callback + await api.createComments(comments, projectId); + }} + onResolve={resolveComments} + onError={handleError} +> + {children} + +``` + +Each comment created within this provider will have the `projectId` field set, which you can use for filtering and organizing comments per project. + ### Programmatic Comment Creation ```tsx @@ -948,9 +981,9 @@ function CustomTool() { ### Error Handling -You can handle errors in two ways: +The `onError` callback is required and handles all error types, including overlay interaction errors like attempting to add a comment while editing or dismissing an empty comment. -**1. Using the `onError` callback (recommended):** +**1. Using the `onError` callback:** ```tsx { - console.error('Comment operation failed:', error); - // Show user-friendly error message - toast.error('Failed to save comments. Please try again.'); + console.error('Comment operation failed:', error.code, error.message); + toast.error(error.message); }} > {children} @@ -1006,7 +1038,10 @@ switch(e.code){ break case "CONFIRM_ERROR": alert('Failed to submit your comments') - break + break + case "INSERT_ERROR": + alert('Failed to insert your comment') + break default: alert('An unknown error occured') } @@ -1176,6 +1211,13 @@ MIT © [Kendew Agency](https://github.com/Kendew-Agency) A list if breaking changes that could impact the way you configured the package +### 0.3.0 +- Added `projectId` prop to `CommentContextProvider`. When set, the project ID is attached to every new comment and passed as a second argument to the `onConfirm` callback. Update your `onConfirm` signature from `(comments) => ...` to `(comments, projectId?) => ...`. +- `onError` is now a required prop on `CommentContextProvider`. If you previously omitted it, you must provide an error handler. +- Added `InsertError` error class and `INSERT_ERROR` error code. The overlay now triggers `onError` instead of using `alert()` when a user tries to add a comment while editing or dismisses an empty comment. +- Added `triggerError` method to the comment context (available via `useComments()`), allowing programmatic error triggering through the `onError` callback. +- `CommentType` now includes an optional `projectId` field. + ### 0.2.0 - Reworked the subscription system. The system remains in beta and may change in the future. Configurations made with version 0.1.2 or older will need adjustment after updating. - `mode` was defined twice in props. It has been removed as a root prop and is now only part of the config. diff --git a/package.json b/package.json index a6d560e..89af483 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kendew-agency/react-feedback-layer", - "version": "0.2.2", + "version": "0.3.0", "description": "Drop-in React feedback layer for collecting contextual user feedback", "license": "MIT", "author": "Kendew Agency", diff --git a/playground/features/comments/comment-wrapper.tsx b/playground/features/comments/comment-wrapper.tsx index e1fa9ca..9daf439 100644 --- a/playground/features/comments/comment-wrapper.tsx +++ b/playground/features/comments/comment-wrapper.tsx @@ -72,6 +72,7 @@ export const CommentWrapper = ({ children }: { children: ReactNode }) => { currentUser={{ name: "John Doe", }} + projectId="proj_1234" onResolve={resolveCommentsInDataBase} onError={(e) => { console.error(e); diff --git a/playground/features/comments/server.ts b/playground/features/comments/server.ts index b715aff..90903cc 100644 --- a/playground/features/comments/server.ts +++ b/playground/features/comments/server.ts @@ -1,7 +1,11 @@ import type { ConfirmedComment } from "../../../src/types"; // Mocks server code -export async function sendCommentsToDataBase(comments: ConfirmedComment[]) { +export async function sendCommentsToDataBase( + comments: ConfirmedComment[], + projectId?: string, +) { + console.info("Project ID:", projectId); console.info("Adding comments:", comments); } diff --git a/src/components/comment-overlay.tsx b/src/components/comment-overlay.tsx index 48a6497..016e4ec 100644 --- a/src/components/comment-overlay.tsx +++ b/src/components/comment-overlay.tsx @@ -1,6 +1,6 @@ import { type CSSProperties, - HTMLProps, + type HTMLProps, type ReactNode, useRef, useState, @@ -8,6 +8,7 @@ import { import { useComments } from "../contexts/comment-context"; import type { Position } from "../types"; import { getRelativePos, normalizeRect } from "../utils/position"; +import { InsertError } from "../errors"; const DRAG_THRESHOLD = 4; @@ -22,6 +23,7 @@ export const CommentOverlay = ({ children, ...rest }: CommentOverlayProps) => { focusOnComment, getActiveComment, changeOverlayState, + triggerError, } = useComments(); const [preview, setPreview] = useState { const onPointerDown = (e: React.PointerEvent) => { if (overlayState === "editing") { - alert( - "Can not add a new comment. You are currently editing a comment. Please finish editing before adding a new one.", + triggerError( + new InsertError( + "You are editing a comment, please finish editing the current comment first", + ), ); return; } @@ -136,6 +140,14 @@ export const CommentOverlay = ({ children, ...rest }: CommentOverlayProps) => {
{ + if (getActiveComment()?.content === "") { + triggerError( + new InsertError( + "Comment is empty, either delete the comment or add some content", + ), + ); + return; + } focusOnComment(null); changeOverlayState("idle"); }} diff --git a/src/contexts/comment-context.tsx b/src/contexts/comment-context.tsx index a439e54..e4656ea 100644 --- a/src/contexts/comment-context.tsx +++ b/src/contexts/comment-context.tsx @@ -9,6 +9,7 @@ import type { CommentVisibility, Indicator, Position, + KnownError, } from "../types"; import { tx } from "../lib/tx"; import { hasStatus } from "../utils/hasStatus"; @@ -34,6 +35,7 @@ const commentReducer = ( ...state.comments, { id: newId, + projectId: state.projectId, user: state.currentUser, position: { x: action.position.x, @@ -160,6 +162,7 @@ export const CommentContextProvider = ({ hideResolving: false, }, }, + projectId, onError, }: CommentOverlayProps) => { const [state, dispatch] = useReducer(commentReducer, { @@ -168,6 +171,7 @@ export const CommentContextProvider = ({ currentUser, focussedComment: null, config, + projectId, } satisfies CommentState); // Replace the initial comments with the comments from the listen query @@ -277,6 +281,7 @@ export const CommentContextProvider = ({ ...c, status: "published", })), + projectId, ), ); @@ -324,6 +329,12 @@ export const CommentContextProvider = ({ dispatch({ type: "CHANGE_OVERLAYSTATE", to: state }); }; + const triggerError = (error: KnownError) => { + if (onError) { + onError(error); + } + }; + return ( {children} diff --git a/src/errors.ts b/src/errors.ts index 8b77d36..1be82e9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -21,6 +21,12 @@ export class UnknownError extends BaseError { } } +export class InsertError extends BaseError { + constructor(message = "An error occured while inserting your comments") { + super(message, "INSERT_ERROR", 500); + } +} + export class ResolveError extends BaseError { constructor(message = "An error occured while resolving your comments") { super(message, "RESOLVE_ERROR", 500); diff --git a/src/types/comment.types.ts b/src/types/comment.types.ts index 10a46bd..fe18050 100644 --- a/src/types/comment.types.ts +++ b/src/types/comment.types.ts @@ -1,3 +1,4 @@ +import type { KnownError } from "./error.types"; import type { CommentOverlayState, CommentVisibility, @@ -40,6 +41,11 @@ export type CommentType = { * @description shows a box around the content a comment is tied to */ indicator?: Indicator | null; + /** + * The id of the project a comment is connected to + * @description useful for filtering comments by project + */ + projectId?: string | undefined; }; export type Status = "draft" | "published" | "resolving" | "resolved"; @@ -113,6 +119,7 @@ export type CommentContext = { error: Error | DOMException | null; }>; updateCommentVisibility: (visibility: Partial) => void; + triggerError: (error: KnownError) => void; }; export type CommentWithStatus = Omit< diff --git a/src/types/error.types.ts b/src/types/error.types.ts index 2cf3d87..de8e10a 100644 --- a/src/types/error.types.ts +++ b/src/types/error.types.ts @@ -3,6 +3,7 @@ import type { ConfirmError, ResolveError } from "../errors"; export type OverlayErrorCode = | "RESOLVE_ERROR" | "CONFIRM_ERROR" - | "UNKNOWN_ERROR"; + | "UNKNOWN_ERROR" + | "INSERT_ERROR"; export type KnownError = ConfirmError | ResolveError; diff --git a/src/types/overlay.types.ts b/src/types/overlay.types.ts index 2e6b9a0..1e86e5a 100644 --- a/src/types/overlay.types.ts +++ b/src/types/overlay.types.ts @@ -33,6 +33,11 @@ export type CommentState = { * Optional configuration for the comment layer */ config?: Config | undefined; + /** + * Attached a project ID to the current overlay + * @description This ID will be passed to the comment actions. You can attach comments to a project by storing this ID in your database + */ + projectId?: string | undefined; }; /** @@ -92,8 +97,12 @@ export type CommentOverlayProps = { * Callback with all newly confirmed comments * * @param comments as the comments pushed by the user + * @param projectId as the project id if provided */ - onConfirm: (comments: ConfirmedComment[]) => Promise; + onConfirm: ( + comments: ConfirmedComment[], + projectId?: string, + ) => Promise; /** * Callback to handle resolved comments * @@ -105,11 +114,20 @@ export type CommentOverlayProps = { * * @param error as the error that occurred */ - onError?: (error: KnownError) => void; + onError: (error: KnownError) => void; /** * Optional configuration for the comment layer */ config?: Config; + /** + * Attached a project ID to the current overlay + * @description This ID will be passed to the comment actions. You can attach comments to a project by storing this ID in your database + * @example + * ```ts + * projectId: "proj_123456789" + * ``` + */ + projectId?: string; }; export interface RealtimeSubscription {