Skip to content

feat(desktop): add feedback button and modal#1103

Closed
Kitenite wants to merge 6 commits into
mainfrom
add-a-feedback-button-next-to-toggle-sidebar
Closed

feat(desktop): add feedback button and modal#1103
Kitenite wants to merge 6 commits into
mainfrom
add-a-feedback-button-next-to-toggle-sidebar

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Feb 1, 2026

Summary

  • Add feedback button (help icon) to the TopBar next to org dropdown
  • Add feedback modal with text input and image attachment support
  • Store feedback submissions in database with user reference
  • Connect "Report Issue" menu item to feedback modal
  • Add links to docs and Discord in the modal

Changes

  • New feedback table in database schema
  • New feedback.create tRPC mutation
  • New FeedbackButton component with modal
  • New feedback-modal Zustand store (persists draft until sent/cleared)
  • Updated OrganizationDropdown to open feedback modal from "Report Issue"

Test plan

  • Click help icon in TopBar - feedback modal opens
  • Enter text and attach images
  • Submit feedback - stored in database
  • Close modal without sending - content persists
  • Click "Report Issue" in dropdown - same modal opens
  • Links to docs and Discord work

Summary by CodeRabbit

  • New Features
    • Added in-app feedback submission modal accessible from the dashboard top bar
    • Users can now attach multiple images to feedback reports with preview and removal options
    • "Report Issue" menu item now uses the in-app feedback form instead of external navigation
    • Feedback submissions are persisted and associated with user accounts

✏️ Tip: You can customize this high-level summary in your review settings.

- Move organization dropdown to right side of TopBar
- Report Issue now opens feedback modal instead of GitHub
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Adds a complete feedback submission system featuring a FeedbackButton component with modal dialog for message and image attachment, a Zustand-based modal state store with persistence, a new PostgreSQL feedback table with user and organization relationships, a TRPC feedback.create mutation for server-side validation and persistence, and integrates the modal trigger into the OrganizationDropdown's Report Issue action.

Changes

Cohort / File(s) Summary
TopBar Integration
TopBar.tsx, components/FeedbackButton/FeedbackButton.tsx, components/FeedbackButton/index.ts
New FeedbackButton component added to TopBar render tree; includes modal dialog with multiline message textarea, image attachment with preview/removal, loading states, and toast notifications on success/error via TRPC mutation.
State Management
apps/desktop/src/renderer/stores/feedback-modal.ts
Zustand store with persisted feedback form state (message, images) and modal visibility; exports 13 convenience selectors for component integration.
OrganizationDropdown Integration
components/TopBar/components/OrganizationDropdown/OrganizationDropdown.tsx
Report Issue menu item behavior changed from external URL link to triggering in-app feedback modal via useOpenFeedbackModal hook.
Backend Router
packages/trpc/src/router/feedback/feedback.ts, packages/trpc/src/router/feedback/index.ts, packages/trpc/src/root.ts
New TRPC feedback router with single create mutation; validates message and images input; inserts record into database; integrated into appRouter.
Database Schema
packages/db/src/schema/schema.ts, packages/db/src/schema/relations.ts
New feedback table with userId, organizationId, message, images (jsonb), metadata columns; foreign keys to users (cascade delete) and organizations (set null); indexes on userId and createdAt; relation exports for users and organizations.
Database Migration
packages/db/drizzle/0017_add_feedback_table.sql, packages/db/drizzle/meta/_journal.json
SQL migration adding feedback table definition with constraints and indexes; journal entry added for migration tracking.

Sequence Diagram

sequenceDiagram
    participant User
    participant FeedbackButton
    participant FeedbackModalStore
    participant TRPC Client
    participant TRPC Server
    participant Database

    User->>FeedbackButton: Click Feedback Button
    FeedbackButton->>FeedbackModalStore: openModal()
    FeedbackModalStore->>FeedbackButton: Modal Opens
    User->>FeedbackButton: Add message & images
    FeedbackButton->>FeedbackModalStore: setMessage(), addImage()
    FeedbackButton->>User: Display preview
    User->>FeedbackButton: Submit
    FeedbackButton->>TRPC Client: feedback.create.mutate({message, images})
    TRPC Client->>TRPC Server: POST /trpc/feedback.create
    TRPC Server->>TRPC Server: Validate input (zod)
    TRPC Server->>Database: INSERT feedback record
    Database->>Database: Create record with FK constraints
    Database->>TRPC Server: Return created row
    TRPC Server->>TRPC Client: Return success response
    TRPC Client->>FeedbackButton: Mutation success
    FeedbackButton->>User: Show success toast
    FeedbackButton->>FeedbackModalStore: clearForm(), closeModal()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • feat(desktop): add help menu to top bar #259: Adds HelpMenu with external Report Issue link to TopBar; this PR replaces that external link behavior with an in-app feedback modal system and implements the complete feedback collection infrastructure.

Poem

🐰 A hop, skip, and feedback form away!
We gather your thoughts throughout the day,
With images attached, no need to stray,
Your insights stored—hooray, hooray!
Modal and store, database array! 📋✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main feature addition: a feedback button and modal in the desktop app.
Description check ✅ Passed The PR description provides a comprehensive summary, detailed changes, and a test plan, though it doesn't strictly follow the template format with labeled sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-a-feedback-button-next-to-toggle-sidebar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/FeedbackButton/FeedbackButton.tsx`:
- Around line 49-71: handleFileSelect currently ignores FileReader failures and
assumes event.target.result is non-null; add an onerror handler on the
FileReader to handle read failures (reader.onerror) and report the error (e.g.,
via console.error or the app's notification/error handler) and avoid calling
addImage on failure. Also guard the onload result so if event.target?.result is
null skip adding the image and report an appropriate error, and ensure
fileInputRef.current.value is still cleared after both success and error paths;
update the FileReader usage in handleFileSelect to include these checks and
error reporting.

In `@apps/desktop/src/renderer/stores/feedback-modal.ts`:
- Around line 22-52: The persist is currently saving images (base64) which can
exceed localStorage quotas; update the useFeedbackModalStore persist config to
avoid persisting images by changing partialize to only include message (remove
images) or alternatively supply a custom storage implementation (e.g.,
IndexedDB) to the persist call; target the persist config and the partialize
function in useFeedbackModalStore and ensure image mutations (addImage,
removeImage, clearForm) continue to operate in-memory on the images array while
only message is written to persistent storage.

In `@packages/trpc/src/router/feedback/feedback.ts`:
- Around line 17-34: The destructured "created" from the
db.insert(...).returning() call in the mutation can be undefined if the returned
array is empty; update the mutation handler (the async function inside
.mutation) to check the returned array before destructuring or after (e.g.,
inspect the array result of db.insert(feedback).values(...).returning()), and if
no record was returned throw a descriptive error (or call ctx.throw / trpcError)
instead of returning undefined; ensure you reference the insert call and the
local variable "created" so the guard covers the db.insert(...).returning()
result and returns a valid object or throws.
🧹 Nitpick comments (3)
packages/db/drizzle/0017_add_feedback_table.sql (2)

1-9: Consider the implications of storing image data URLs directly in the database.

Storing base64-encoded image data URLs in the images jsonb column can lead to very large row sizes, potentially impacting:

  • Query performance when selecting feedback records
  • Database storage costs
  • Backup/restore times

For a feedback feature with moderate volume, this may be acceptable. However, if feedback volume grows, consider uploading images to object storage (S3, etc.) and storing only the URLs.


13-14: Consider adding an index on organization_id if org-scoped queries are expected.

The migration creates indexes on user_id and created_at, but not on organization_id. If you anticipate querying feedback by organization (e.g., admin dashboard filtering by org), an index would improve performance.

📊 Suggested index addition
CREATE INDEX "feedback_organization_id_idx" ON "feedback" USING btree ("organization_id");
packages/trpc/src/router/feedback/feedback.ts (1)

10-15: Consider adding size limits for image data URLs.

The input validation limits the array to 10 images but doesn't restrict individual string sizes. Since images are stored as base64 data URLs, a single image could be several megabytes. Consider adding a max length per image string to prevent excessively large payloads.

🛡️ Suggested validation enhancement
 		.input(
 			z.object({
 				message: z.string().min(1).max(10000),
-				images: z.array(z.string()).max(10).default([]),
+				images: z.array(z.string().max(5_000_000)).max(10).default([]), // ~5MB per image
 				metadata: z.record(z.string(), z.unknown()).optional(),
 			}),
 		)

Comment on lines +49 to +71
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;

for (const file of files) {
if (!file.type.startsWith("image/")) continue;

const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target?.result as string;
addImage({
id: crypto.randomUUID(),
dataUrl,
name: file.name,
});
};
reader.readAsDataURL(file);
}

if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for FileReader failures.

The FileReader.onerror callback is not handled. If a file read fails (e.g., corrupted file, permission issue), the user receives no feedback. Additionally, event.target?.result could be null.

🛡️ Proposed fix with error handling
 	const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
 		const files = e.target.files;
 		if (!files) return;

 		for (const file of files) {
 			if (!file.type.startsWith("image/")) continue;

 			const reader = new FileReader();
 			reader.onload = (event) => {
-				const dataUrl = event.target?.result as string;
+				const dataUrl = event.target?.result;
+				if (typeof dataUrl !== "string") return;
 				addImage({
 					id: crypto.randomUUID(),
 					dataUrl,
 					name: file.name,
 				});
 			};
+			reader.onerror = () => {
+				console.error("[feedback] Failed to read file:", file.name);
+				toast.error(`Failed to read ${file.name}`);
+			};
 			reader.readAsDataURL(file);
 		}

 		if (fileInputRef.current) {
 			fileInputRef.current.value = "";
 		}
 	};
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/FeedbackButton/FeedbackButton.tsx`
around lines 49 - 71, handleFileSelect currently ignores FileReader failures and
assumes event.target.result is non-null; add an onerror handler on the
FileReader to handle read failures (reader.onerror) and report the error (e.g.,
via console.error or the app's notification/error handler) and avoid calling
addImage on failure. Also guard the onload result so if event.target?.result is
null skip adding the image and report an appropriate error, and ensure
fileInputRef.current.value is still cleared after both success and error paths;
update the FileReader usage in handleFileSelect to include these checks and
error reporting.

Comment on lines +22 to +52
export const useFeedbackModalStore = create<FeedbackModalState>()(
devtools(
persist(
(set) => ({
isOpen: false,
message: "",
images: [],

openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),

setMessage: (message) => set({ message }),

addImage: (image) =>
set((state) => ({ images: [...state.images, image] })),

removeImage: (id) =>
set((state) => ({
images: state.images.filter((img) => img.id !== id),
})),

clearForm: () => set({ message: "", images: [] }),
}),
{
name: "feedback-form-storage",
partialize: (state) => ({
message: state.message,
images: state.images,
}),
},
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persisting image data URLs can exceed storage quotas and break rehydration.
persist defaults to localStorage; storing base64 images there can easily hit quota and throw, which prevents draft persistence. Consider moving image persistence to a larger store (e.g., IndexedDB) or exclude images from persisted state and keep them in-memory only.

💡 Minimal mitigation (exclude images from persisted state)
 			{
 				name: "feedback-form-storage",
 				partialize: (state) => ({
 					message: state.message,
-					images: state.images,
 				}),
 			},
Zustand persist default storage (localStorage) size limits and how to configure custom storage (e.g., IndexedDB)
🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/stores/feedback-modal.ts` around lines 22 - 52, The
persist is currently saving images (base64) which can exceed localStorage
quotas; update the useFeedbackModalStore persist config to avoid persisting
images by changing partialize to only include message (remove images) or
alternatively supply a custom storage implementation (e.g., IndexedDB) to the
persist call; target the persist config and the partialize function in
useFeedbackModalStore and ensure image mutations (addImage, removeImage,
clearForm) continue to operate in-memory on the images array while only message
is written to persistent storage.

Comment on lines +17 to +34
.mutation(async ({ ctx, input }) => {
const [created] = await db
.insert(feedback)
.values({
userId: ctx.session.user.id,
organizationId: ctx.session.session.activeOrganizationId,
message: input.message,
images: input.images,
metadata: input.metadata,
})
.returning();

console.log(
"[feedback/create] Feedback submitted by user:",
ctx.session.user.id,
);

return created;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle potential undefined return from destructuring.

The destructured created could be undefined if the insert somehow returns an empty array (though Drizzle typically throws on failure). Adding a guard ensures type safety and prevents returning undefined to the client.

🛡️ Proposed fix
 		.mutation(async ({ ctx, input }) => {
 			const [created] = await db
 				.insert(feedback)
 				.values({
 					userId: ctx.session.user.id,
 					organizationId: ctx.session.session.activeOrganizationId,
 					message: input.message,
 					images: input.images,
 					metadata: input.metadata,
 				})
 				.returning();

+			if (!created) {
+				throw new Error("Failed to create feedback record");
+			}
+
 			console.log(
 				"[feedback/create] Feedback submitted by user:",
 				ctx.session.user.id,
 			);

 			return created;
 		}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.mutation(async ({ ctx, input }) => {
const [created] = await db
.insert(feedback)
.values({
userId: ctx.session.user.id,
organizationId: ctx.session.session.activeOrganizationId,
message: input.message,
images: input.images,
metadata: input.metadata,
})
.returning();
console.log(
"[feedback/create] Feedback submitted by user:",
ctx.session.user.id,
);
return created;
.mutation(async ({ ctx, input }) => {
const [created] = await db
.insert(feedback)
.values({
userId: ctx.session.user.id,
organizationId: ctx.session.session.activeOrganizationId,
message: input.message,
images: input.images,
metadata: input.metadata,
})
.returning();
if (!created) {
throw new Error("Failed to create feedback record");
}
console.log(
"[feedback/create] Feedback submitted by user:",
ctx.session.user.id,
);
return created;
}),
🤖 Prompt for AI Agents
In `@packages/trpc/src/router/feedback/feedback.ts` around lines 17 - 34, The
destructured "created" from the db.insert(...).returning() call in the mutation
can be undefined if the returned array is empty; update the mutation handler
(the async function inside .mutation) to check the returned array before
destructuring or after (e.g., inspect the array result of
db.insert(feedback).values(...).returning()), and if no record was returned
throw a descriptive error (or call ctx.throw / trpcError) instead of returning
undefined; ensure you reference the insert call and the local variable "created"
so the guard covers the db.insert(...).returning() result and returns a valid
object or throws.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 1, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

@Kitenite
Copy link
Copy Markdown
Collaborator Author

Closing — stale and conflicting, will restart fresh if needed.

@Kitenite Kitenite closed this Feb 17, 2026
@Kitenite Kitenite deleted the add-a-feedback-button-next-to-toggle-sidebar branch February 19, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant