Skip to content

feat(config-editor): diff preview before saving (#133)#217

Merged
BillChirico merged 9 commits intomainfrom
feat/issue-133
Mar 2, 2026
Merged

feat(config-editor): diff preview before saving (#133)#217
BillChirico merged 9 commits intomainfrom
feat/issue-133

Conversation

@BillChirico
Copy link
Collaborator

Summary

Closes #133

Implements all subtasks from the issue: diff preview modal before saving, per-section revert, unsaved-changes indicator, Ctrl+S diff preview, and undo-after-save.

Changes

New Component: ConfigDiffModal

  • Dialog that shows before saving with scrollable diff view
  • Changed sections listed as badges with individual Revert buttons
  • Cancel / Confirm Save actions
  • Spinner + disabled state while saving

Updated: ConfigEditor

Feature Details
Diff modal Save button + Ctrl+S open the modal instead of saving directly
Per-section revert Each changed section badge has a revert button in the modal
Unsaved-changes indicator Yellow dot on Save button when draft differs from saved
Undo last save "Undo Last Save" button appears after a successful save (1 level deep)
Inline diff Existing inline diff view retained below the form as live preview

Tests

  • 5 tests for ConfigDiff: no-changes state, added/removed counts, titles, aria region
  • 8 tests for ConfigDiffModal: render, section badges, revert, confirm, cancel, disabled states

Checklist

  • All acceptance criteria met
  • TypeScript clean (0 errors in our files)
  • Biome lint: 0 errors, 0 warnings in our files
  • 154/154 tests passing
  • Committed after each logical change

- Add ConfigDiffModal component with scrollable diff view
- Show changed sections as badges with per-section revert buttons
- Save button opens diff modal for review instead of saving directly
- Ctrl+S now triggers diff preview modal
- Add unsaved-changes indicator dot on Save button
- Add undo-last-save button after successful save (1 level deep)
- Inline diff view remains below the form as a live preview
…133)

- Add showDiffModal/prevSavedConfig state
- Add changedSections computed from patches
- openDiffModal() replaces direct save — Ctrl+S also opens modal
- executeSave() is the actual save logic (called from modal confirm)
- revertSection() reverts individual top-level sections
- undoLastSave() restores config to pre-save state
- Save button indicator dot when unsaved changes exist
- Undo Last Save button shown after successful save
- Fix a11y: switched fieldset for changed-sections group
- Biome format applied
- 5 tests for ConfigDiff: no-changes, added/removed counts, custom title,
  default title, diff view aria region
- 8 tests for ConfigDiffModal: renders, section badges, revert per section,
  confirm save, cancel, disabled while saving, saving text, empty sections
Copilot AI review requested due to automatic review settings March 2, 2026 04:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3e6c1 and e2e2de1.

📒 Files selected for processing (5)
  • tests/modules/commandAliases.test.js
  • web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts
  • web/src/components/dashboard/ai-feedback-stats.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/src/types/analytics.ts
📝 Walkthrough

Walkthrough

The pull request adds a server-side API route for guild AI feedback statistics that proxies to the bot API, updates the dashboard component to call this internal endpoint, introduces a ConfigDiffModal component for previewing configuration changes before saving, and integrates this modal into the config editor with per-section patching and undo functionality. Supporting tests are included.

Changes

Cohort / File(s) Summary
AI Feedback Stats API Route
web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts
New server route that validates guildId, authorizes admin access, and proxies GET requests to the bot API with support for days query parameter. Includes error handling for missing guildId and authorization failures.
AI Feedback Stats Component
web/src/components/dashboard/ai-feedback-stats.tsx
Refactored to obtain guildId directly from hook, call internal API path /api/guilds/{guildId}/ai-feedback/stats instead of bot API directly, and hardened pie chart label calculations for null values.
Config Diff Modal
web/src/components/dashboard/config-diff-modal.tsx, web/tests/components/dashboard/config-diff-modal.test.tsx
New modal component that previews pending configuration changes with changed sections as badges, per-section revert buttons, scrollable diff view, and save/cancel actions. Test suite validates rendering, callbacks, and disabled states during saving.
Config Editor Integration
web/src/components/dashboard/config-editor.tsx
Major refactor replacing immediate save with modal-based flow. Adds per-section patching, undo functionality with "Undo Last Save" button, unsaved changes indicator on Save button, section-level revert capability, inline diff view, and helper functions for diff computation and patching.
Config Diff Tests
web/tests/components/dashboard/config-diff.test.tsx
Test coverage for ConfigDiff component validating diff visibility, added/removed line counts, custom and default titles, and region labeling.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(config-editor): diff preview before saving (#133)' directly and clearly describes the main change: adding a diff preview modal before saving in the config editor.
Description check ✅ Passed The description comprehensively explains the implemented features, references the closing issue (#133), details all subtasks completed, and documents the changes made.
Linked Issues check ✅ Passed All six subtasks from issue #133 are implemented: diff computation via patches, diff modal with preview, per-section revert buttons, unsaved-changes dot indicator, Ctrl+S keyboard shortcut, and undo-last-save feature with one-level storage.
Out of Scope Changes check ✅ Passed One commit fixes a TypeScript error and adds a missing API route for /guilds/[guildId]/ai-feedback/stats, while another updates ai-feedback-stats.tsx to use the new endpoint—these are supporting changes necessary for the PR's functionality.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/issue-133

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.

@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Implements explicit save flow with diff preview modal, replacing auto-save with user-controlled saves. New ConfigDiffModal component shows pending changes with per-section revert capability before confirming. Config editor adds unsaved-changes indicator (yellow dot), Ctrl+S shortcut, and 1-level undo functionality.

Key improvements:

  • Explicit save confirmation prevents accidental overwrites
  • Per-section revert gives granular control over changes
  • Undo last save provides safety net for mistaken saves
  • Comprehensive test coverage (13 new tests)

Side improvements:

  • ai-feedback-stats now uses Next.js proxy route with input validation (days clamped to 1-90)
  • Fixed React key prop to use stable identifiers
  • Centralized type definitions in analytics.ts

Issues found:

  • config-editor.tsx:430 - useEffect dependency array missing guildId, preventing undo snapshot cleanup on guild change (runtime guard exists but effect comment is misleading)
  • ai-feedback-stats.tsx:53 - Type name inconsistency between import alias and usage

Confidence Score: 4/5

  • Safe to merge with minor fixes - one logic issue with effect dependencies and one type inconsistency
  • Core functionality is solid with comprehensive tests and good architecture, but the useEffect dependency bug could cause stale undo snapshots across guild switches, and the type inconsistency affects code maintainability
  • web/src/components/dashboard/config-editor.tsx needs the useEffect dependency fix, web/src/components/dashboard/ai-feedback-stats.tsx needs type name consistency fix

Important Files Changed

Filename Overview
web/src/components/dashboard/config-diff-modal.tsx New component implementing diff preview modal with section badges, revert buttons, and save confirmation - well structured with good accessibility
web/src/components/dashboard/config-editor.tsx Major refactor removing auto-save and adding explicit save flow with diff modal, undo functionality, and per-section revert - has useEffect dependency issue
web/src/components/dashboard/ai-feedback-stats.tsx Refactored to use proxy route instead of direct bot API, fixed React key prop, moved type to shared file - minor type name inconsistency
web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts Added input validation and clamping for days parameter (1-90 range) with proper error handling

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User Edits Config] --> B{Has Changes?}
    B -->|No| C[Save Button Disabled]
    B -->|Yes| D[Save Button Active with Yellow Dot]
    D --> E[User Clicks Save or Ctrl+S]
    E --> F[ConfigDiffModal Opens]
    F --> G[Show Changed Sections with Revert Buttons]
    G --> H{User Action?}
    H -->|Cancel| I[Close Modal, Keep Draft]
    H -->|Revert Section| J[Restore Section from Saved]
    J --> G
    H -->|Confirm Save| K[Execute Save]
    K --> L{Save Success?}
    L -->|Partial Failure| M[Update Draft with Succeeded Sections]
    L -->|Full Success| N[Store Previous Config for Undo]
    N --> O[Reload from Server]
    O --> P[Show Undo Last Save Button]
    P --> Q{User Clicks Undo?}
    Q -->|Yes| R[Restore Previous Config to Draft]
    R --> D
    Q -->|No| S[Continue Editing]
Loading

Last reviewed commit: e2e2de1

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

… route

- Replace incorrect useGuildSelection() destructuring ({selectedGuild, apiBase})
  with correct string return value (guildId)
- Add missing /api/guilds/[guildId]/ai-feedback/stats Next.js proxy route
  so the component fetches via server-side proxy instead of direct bot API
- Fix 'percent' possibly undefined in Pie chart label callback
- Fix noArrayIndexKey lint warning by using entry.name as Cell key
- Apply biome formatter fixes
Copy link
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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/app/api/guilds/`[guildId]/ai-feedback/stats/route.ts:
- Around line 12-52: Create a shared analytics response type in
web/src/types/analytics.ts (e.g., export type AiFeedbackStatsResponse = { ... })
that matches the current stats payload returned by the bot API, then update this
route's GET handler to import and use that type for its response contract
(reference GET in web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts)
and ensure the dashboard consumer also imports the same type from
web/src/types/analytics.ts so both API flow (proxyToBotApi, buildUpstreamUrl)
and dashboard UI share the exact contract to avoid drift.
- Around line 38-44: Validate and clamp the 'days' query parameter before
proxying it: in the loop that reads request.nextUrl.searchParams.get('days')
(where you currently call upstreamUrl.searchParams.set), parse the value to an
integer (e.g., via Number.parseInt or Number(value)), check it's a finite
positive integer, then clamp it to an acceptable range (for example min 1, max
90) and only then set upstreamUrl.searchParams.set('days',
String(clampedValue)); if parsing fails or the value is out-of-range, either
omit the param or use a safe default (e.g., 7) instead of forwarding the raw
value.

In `@web/src/components/dashboard/config-editor.tsx`:
- Around line 127-129: prevSavedConfig is stored globally so switching guilds
lets "Undo Last Save" apply another guild's snapshot; change prevSavedConfig to
be guild-scoped by storing snapshots keyed by guild id (e.g.,
useState<Record<string, GuildConfig | null>> or a Map) and update all places
that read/write prevSavedConfig to instead use the current guildId key
(references: prevSavedConfig, setPrevSavedConfig, and any handlers like the
Undo/Save handlers around lines ~413-415, ~426-435, ~765-776). Ensure when
switching guilds you read the snapshot for that guildId (or default null) and
when saving/creating an undo snapshot you write it to the guildId key so
snapshots cannot be applied across guilds.

In `@web/tests/components/dashboard/config-diff-modal.test.tsx`:
- Around line 57-67: Tests for ConfigDiffModal check button disabling but miss
asserting that dialog-level close attempts are blocked while saving; add a
regression test that renders <ConfigDiffModal {...baseProps} saving={true} />
with baseProps.onOpenChange mocked, simulate a dialog-close attempt (e.g.,
Escape key press and/or outside click using fireEvent/userEvent against the
document) and assert baseProps.onOpenChange was not called; reference the
ConfigDiffModal component, the saving prop, and the onOpenChange handler to
locate where to add this test.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcf04e2 and 1b3e6c1.

📒 Files selected for processing (6)
  • web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts
  • web/src/components/dashboard/ai-feedback-stats.tsx
  • web/src/components/dashboard/config-diff-modal.tsx
  • web/src/components/dashboard/config-editor.tsx
  • web/tests/components/dashboard/config-diff-modal.test.tsx
  • web/tests/components/dashboard/config-diff.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Docker Build Validation
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript with type safety. Share contracts between dashboard UI and API responses via web/src/types/analytics.ts and similar type definition files

Files:

  • web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts
web/src/components/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Component files should integrate with Zustand stores for state management (e.g., discord-entities store for caching Discord channels and roles per guild)

Files:

  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/ai-feedback-stats.tsx
  • web/src/components/dashboard/config-diff-modal.tsx
🧠 Learnings (1)
📚 Learning: 2026-03-01T06:03:34.399Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-01T06:03:34.399Z
Learning: Applies to src/modules/*.js : Per-request modules (AI, spam, moderation) should call `getConfig(guildId)` on every invocation for automatic config changes. Stateful resources should use `onConfigChange` listeners for reactive updates

Applied to files:

  • web/src/components/dashboard/config-editor.tsx
  • web/src/components/dashboard/ai-feedback-stats.tsx
🧬 Code graph analysis (3)
web/tests/components/dashboard/config-diff.test.tsx (1)
web/src/components/dashboard/config-diff.tsx (1)
  • ConfigDiff (34-120)
web/src/app/api/guilds/[guildId]/ai-feedback/stats/route.ts (1)
web/src/lib/bot-api-proxy.ts (4)
  • authorizeGuildAdmin (37-69)
  • getBotApiConfig (82-92)
  • buildUpstreamUrl (100-113)
  • proxyToBotApi (135-176)
web/src/components/dashboard/ai-feedback-stats.tsx (2)
src/api/routes/ai-feedback.js (3)
  • guildId (94-94)
  • guildId (190-190)
  • stats (104-107)
web/src/hooks/use-guild-selection.ts (1)
  • useGuildSelection (12-54)
🔇 Additional comments (5)
web/src/components/dashboard/ai-feedback-stats.tsx (2)

40-76: Good API wiring update and guild guard.

Switching to guildId-based internal API calls with encodeURIComponent and early null return is clean and safe.


147-154: Nice defensive rendering improvement.

The percent ?? 0 fallback and stable Cell key improve chart resilience.

web/src/components/dashboard/config-diff-modal.tsx (1)

61-122: Modal interaction locking while saving is well implemented.

Disabling close/revert/confirm paths during saving is solid and prevents conflicting user actions.

web/tests/components/dashboard/config-diff.test.tsx (1)

5-39: Test coverage for core ConfigDiff paths looks good.

The suite hits both semantic output and accessibility-relevant rendering states.

web/src/components/dashboard/config-editor.tsx (1)

2327-2339: The modal wiring is clean and matches the new save workflow.

Passing changedSections, confirm, and per-section revert callbacks into ConfigDiffModal is well structured.

Copilot AI review requested due to automatic review settings March 2, 2026 09:37
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@BillChirico BillChirico merged commit 722dbbb into main Mar 2, 2026
6 of 10 checks passed
@BillChirico BillChirico deleted the feat/issue-133 branch March 2, 2026 11:51
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 430 to +432
useEffect(() => {
// Don't auto-save during initial config load or if no changes
if (isLoadingConfigRef.current || !hasChanges || hasValidationErrors || saving) return;
setPrevSavedConfig(null);
}, []);
Copy link

Choose a reason for hiding this comment

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

Effect should depend on guildId to actually clear when guild changes

Suggested change
useEffect(() => {
// Don't auto-save during initial config load or if no changes
if (isLoadingConfigRef.current || !hasChanges || hasValidationErrors || saving) return;
setPrevSavedConfig(null);
}, []);
useEffect(() => {
setPrevSavedConfig(null);
}, [guildId]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/dashboard/config-editor.tsx
Line: 430-432

Comment:
Effect should depend on `guildId` to actually clear when guild changes

```suggestion
  useEffect(() => {
    setPrevSavedConfig(null);
  }, [guildId]);
```

How can I resolve this? If you propose a fix, please make it concise.

}

const data = (await res.json()) as FeedbackStats;
const data = (await res.json()) as AiFeedbackStats;
Copy link

Choose a reason for hiding this comment

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

Type name inconsistency - import renames to AiFeedbackStatsType but assertion uses AiFeedbackStats

Suggested change
const data = (await res.json()) as AiFeedbackStats;
const data = (await res.json()) as AiFeedbackStatsType;
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/dashboard/ai-feedback-stats.tsx
Line: 53

Comment:
Type name inconsistency - import renames to `AiFeedbackStatsType` but assertion uses `AiFeedbackStats`

```suggestion
      const data = (await res.json()) as AiFeedbackStatsType;
```

How can I resolve this? If you propose a fix, please make it concise.

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.

feat: config editor — diff preview before saving changes

2 participants