Refactor: extract shared frontend components to remove duplication#142
Conversation
The same Reset button + color grid pattern was duplicated 6 times across formatting-toolbar.tsx (2x) and docs-formatting-toolbar.tsx (4x). Extract into a shared ColorPickerGrid component. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The create and edit datasource dialogs had identical form field layouts (7 inputs + SSL switch). Extract into a shared component parameterized by idPrefix and passwordPlaceholder. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The same 4-item alignment dropdown (Left/Center/Right/Justify with keyboard shortcuts) was duplicated in the header/footer and body sections. Extract into a local AlignmentDropdown component. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The same verticalAlign/align calculation from legendPosition was duplicated across all 5 chart renderers (line, bar, area, pie, scatter). Extract into a getLegendProps helper in chart-registry. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 45 minutes and 47 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThis PR consolidates common patterns across multiple packages into reusable utilities and components. Backend document handling shifts error handling from controllers to the service layer via a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Verification: verify:selfResult: ✅ PASS in 114.5s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Add DocumentService.getDocumentOrThrow() to replace the repeated document-not-found check across controllers (documents, tabs, cells). Move VALID_ID_PATTERN to image.constants.ts so both image controllers share the same regex instead of defining it independently. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The same nested loop scanning paginatedLayout.pages for a matching blockIndex + lineIndex was duplicated 4 times across selection.ts and peer-cursor.ts. Extract into findPageLine() in pagination.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
conditional-format.ts had a local cloneRange identical to the one already exported from coordinates.ts — replaced with the import. Both conditional-format.ts and range-styles.ts had local normalizeRange functions that duplicate toRange() — removed them and use toRange() directly. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/docs/src/view/selection.ts (1)
560-568:⚠️ Potential issue | 🟡 MinorReturn early when the nested outer row cannot be resolved.
If
findPageLinemisses,baseYstays0, and the subsequent nestedrowYMapcan render selection highlights at the wrong canvas position.🐛 Proposed fix
let baseY = 0; if (outerRowIndex >= 0) { const found = findPageLine(paginatedLayout, blockIndex, outerRowIndex); - if (found) { - baseY = found.pageY + found.pageLine.y + nestedYOffset; - } + if (!found) return []; + baseY = found.pageY + found.pageLine.y + nestedYOffset; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/docs/src/view/selection.ts` around lines 560 - 568, If outerRowIndex >= 0 and findPageLine(paginatedLayout, blockIndex, outerRowIndex) returns null/undefined, return early instead of leaving baseY at 0; locate the block using findPageLine and the variables outerRowIndex, paginatedLayout, blockIndex and nestedYOffset, and if not found bail out (e.g., return) before computing or using the nested rowYMap so selection highlights are not rendered at the wrong Y position.
🧹 Nitpick comments (6)
packages/frontend/src/app/spreadsheet/charts/chart-registry.ts (1)
138-152: LGTM — clean centralization of legend prop mapping.The helper correctly narrows
legendPositiontoverticalAlignfortop/bottomandalignforleft/right, leaving othersundefined. The"none"case is defensively handled even though all current call sites guard withlegendPosition !== "none".Optional: since each renderer's
Props.legendPositionduplicates the same string union, consider having them import and reuse the newly exportedLegendPositiontype to keep the union in a single place.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/charts/chart-registry.ts` around lines 138 - 152, The code centralizes the legend-position union into the exported LegendPosition type but renderers still duplicate the string union; update each renderer Props that currently declares its own legendPosition union to import and use the shared LegendPosition type (reference: LegendPosition and getLegendProps, and chart renderer Prop declarations such as Props.legendPosition or ChartRendererProps) so the union is maintained in one place—replace local literal unions with the imported LegendPosition and run type-checks to ensure no ABI changes.packages/backend/src/api/v1/documents.controller.ts (1)
54-80: Potential optimization: fold the existence check into the write itself.
updateandremovenow issue two DB round-trips — afindUniqueviagetDocumentOrThrowpurely to enforce workspace scoping, followed byupdateDocument/deleteDocumentkeyed only byid. Because the subsequent write ignoresworkspaceId, the pre-check is load‑bearing for authorization, not just ergonomics, so it can't simply be dropped. You can collapse both concerns into a single query withupdateMany/deleteManyusing a compoundwhere, treating a zerocountasNotFoundException:♻️ Sketch
// service async updateDocumentScoped(where: Prisma.DocumentWhereInput, data: Prisma.DocumentUpdateInput) { const { count } = await this.prisma.document.updateMany({ where, data }); if (!count) throw new NotFoundException('Document not found'); // optionally re-fetch to return the updated row return this.prisma.document.findFirst({ where }); }Non-blocking; only worth doing if these endpoints are hot. Otherwise the current shape is clear and consistent with the other controllers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/src/api/v1/documents.controller.ts` around lines 54 - 80, Both update and remove perform two DB round-trips: getDocumentOrThrow (findUnique) then updateDocument/deleteDocument keyed only by id; fold existence + workspace scoping into a single write by adding service methods that use Prisma's updateMany/deleteMany with a compound where (id + workspaceId), e.g. implement documentService.updateDocumentScoped and documentService.deleteDocumentScoped that call updateMany/deleteMany, throw NotFoundException when count === 0, and optionally re-fetch with findFirst if the controller needs the updated row; then have the controller's update and remove call these new scoped methods instead of getDocumentOrThrow followed by updateDocument/deleteDocument.packages/frontend/src/components/datasource-form-fields.tsx (3)
92-101: MissingautoCompleteon the password input.Without
autoComplete="new-password"(or"current-password"depending on context), browsers may offer to autofill stored credentials into this connection-password field, or prompt to save the DB password as a site login. Since this component is shared by both create and edit flows,autoComplete="new-password"is the safer default and can be overridden per-call later if needed.🛡️ Suggested change
<Input id={`${idPrefix}-password`} type="password" + autoComplete="new-password" placeholder={passwordPlaceholder} value={password} onChange={(e) => onPasswordChange(e.target.value)} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/datasource-form-fields.tsx` around lines 92 - 101, The password Input in the datasource form (the Input element with id built from idPrefix, props value={password}, onChange calling onPasswordChange, and placeholder passwordPlaceholder) is missing an autoComplete attribute; add autoComplete="new-password" to that Input component so browsers treat it as a new/connection password by default (this is safe for both create/edit flows and can be overridden by callers if needed).
63-71: Port input accepts any numeric string — considermin/maxand non-integer guard.
type="number"allows negatives, decimals, and exponent notation (e.g.1e5), all of whichNumber(port)will happily convert before sending to the backend. Since TCP ports are integers in[1, 65535], addingmin={1} max={65535} step={1}(and optionallyinputMode="numeric") would prevent obviously-invalid values reachingcreateWorkspaceDataSource/updateDataSource.♻️ Suggested change
<Input id={`${idPrefix}-port`} type="number" + min={1} + max={65535} + step={1} + inputMode="numeric" value={port} onChange={(e) => onPortChange(e.target.value)} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/datasource-form-fields.tsx` around lines 63 - 71, The Port input currently allows negatives, decimals and exponent notation; update the Input element in datasource-form-fields (the Input with id `${idPrefix}-port`) to include min={1} max={65535} step={1} and inputMode="numeric" to constrain UI input, and add a non-integer/invalid-value guard in the onPortChange handler (or the function that receives it) to coerce/validate to an integer in range before passing to createWorkspaceDataSource/updateDataSource (reject or clamp values outside [1,65535] and strip decimals so only integers are sent).
5-22: Optional: collapse the 7on*Changeprops into a singleonChange(field, value)or avalues/onValuesChangepair.The props interface is already 17 fields wide and will grow linearly with every new datasource field. A single discriminated
onChangecallback (orvalues: {...}+onValuesChange: (patch) => void) would reduce the prop surface at both call sites and remove the parallel-array-style maintenance burden when adding fields. Not blocking — current shape is fine and explicit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/datasource-form-fields.tsx` around lines 5 - 22, The props interface DataSourceFormFieldsProps is verbose due to seven parallel on*Change callbacks; replace them with a single unified change API by either adding onChange(field: string, value: string | boolean) and removing onNameChange/onHostChange/onPortChange/onDatabaseChange/onUsernameChange/onPasswordChange/onSslEnabledChange, or by switching to values: { name, host, port, database, username, password, sslEnabled } plus onValuesChange(patch: Partial<typeof values>), then update the component (where it currently calls the individual handlers) to call the new onChange or onValuesChange and update all call sites to pass the consolidated callback/values shape; keep idPrefix, passwordPlaceholder and other unrelated props unchanged.packages/frontend/src/components/datasource-dialog.tsx (1)
144-160: Consider passing an explicitpasswordPlaceholderfor the create dialog.On the edit dialog the placeholder communicates "Leave blank to keep current", but here no placeholder is passed, so the password field renders with no hint at all. Previously the inline JSX may have had a placeholder; worth confirming the create form still gives users a cue (e.g.
passwordPlaceholder="Password").🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/datasource-dialog.tsx` around lines 144 - 160, The DataSourceFormFields in the create dialog is missing an explicit passwordPlaceholder prop so the password input shows no hint; update the DataSourceFormFields usage in the create dialog to include passwordPlaceholder (e.g., passwordPlaceholder="Password" or another appropriate cue) so the password field renders a helpful placeholder; reference the DataSourceFormFields component and its passwordPlaceholder prop when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/src/components/color-picker-grid.tsx`:
- Around line 12-26: The color swatch buttons lack accessible names and a button
type; update the button rendered inside colors.map in color-picker-grid.tsx to
include type="button" and an accessible label (e.g., aria-label or
aria-labelledby) that identifies the color (use the color string or a derived
human-readable name) so screen readers can announce the swatch, and keep the
existing onSelect handler intact; also add type="button" to the Reset button for
consistency with onReset.
---
Outside diff comments:
In `@packages/docs/src/view/selection.ts`:
- Around line 560-568: If outerRowIndex >= 0 and findPageLine(paginatedLayout,
blockIndex, outerRowIndex) returns null/undefined, return early instead of
leaving baseY at 0; locate the block using findPageLine and the variables
outerRowIndex, paginatedLayout, blockIndex and nestedYOffset, and if not found
bail out (e.g., return) before computing or using the nested rowYMap so
selection highlights are not rendered at the wrong Y position.
---
Nitpick comments:
In `@packages/backend/src/api/v1/documents.controller.ts`:
- Around line 54-80: Both update and remove perform two DB round-trips:
getDocumentOrThrow (findUnique) then updateDocument/deleteDocument keyed only by
id; fold existence + workspace scoping into a single write by adding service
methods that use Prisma's updateMany/deleteMany with a compound where (id +
workspaceId), e.g. implement documentService.updateDocumentScoped and
documentService.deleteDocumentScoped that call updateMany/deleteMany, throw
NotFoundException when count === 0, and optionally re-fetch with findFirst if
the controller needs the updated row; then have the controller's update and
remove call these new scoped methods instead of getDocumentOrThrow followed by
updateDocument/deleteDocument.
In `@packages/frontend/src/app/spreadsheet/charts/chart-registry.ts`:
- Around line 138-152: The code centralizes the legend-position union into the
exported LegendPosition type but renderers still duplicate the string union;
update each renderer Props that currently declares its own legendPosition union
to import and use the shared LegendPosition type (reference: LegendPosition and
getLegendProps, and chart renderer Prop declarations such as
Props.legendPosition or ChartRendererProps) so the union is maintained in one
place—replace local literal unions with the imported LegendPosition and run
type-checks to ensure no ABI changes.
In `@packages/frontend/src/components/datasource-dialog.tsx`:
- Around line 144-160: The DataSourceFormFields in the create dialog is missing
an explicit passwordPlaceholder prop so the password input shows no hint; update
the DataSourceFormFields usage in the create dialog to include
passwordPlaceholder (e.g., passwordPlaceholder="Password" or another appropriate
cue) so the password field renders a helpful placeholder; reference the
DataSourceFormFields component and its passwordPlaceholder prop when making the
change.
In `@packages/frontend/src/components/datasource-form-fields.tsx`:
- Around line 92-101: The password Input in the datasource form (the Input
element with id built from idPrefix, props value={password}, onChange calling
onPasswordChange, and placeholder passwordPlaceholder) is missing an
autoComplete attribute; add autoComplete="new-password" to that Input component
so browsers treat it as a new/connection password by default (this is safe for
both create/edit flows and can be overridden by callers if needed).
- Around line 63-71: The Port input currently allows negatives, decimals and
exponent notation; update the Input element in datasource-form-fields (the Input
with id `${idPrefix}-port`) to include min={1} max={65535} step={1} and
inputMode="numeric" to constrain UI input, and add a non-integer/invalid-value
guard in the onPortChange handler (or the function that receives it) to
coerce/validate to an integer in range before passing to
createWorkspaceDataSource/updateDataSource (reject or clamp values outside
[1,65535] and strip decimals so only integers are sent).
- Around line 5-22: The props interface DataSourceFormFieldsProps is verbose due
to seven parallel on*Change callbacks; replace them with a single unified change
API by either adding onChange(field: string, value: string | boolean) and
removing
onNameChange/onHostChange/onPortChange/onDatabaseChange/onUsernameChange/onPasswordChange/onSslEnabledChange,
or by switching to values: { name, host, port, database, username, password,
sslEnabled } plus onValuesChange(patch: Partial<typeof values>), then update the
component (where it currently calls the individual handlers) to call the new
onChange or onValuesChange and update all call sites to pass the consolidated
callback/values shape; keep idPrefix, passwordPlaceholder and other unrelated
props unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9d08a71c-a34b-4509-ad43-5805bb26e671
📒 Files selected for processing (24)
packages/backend/src/api/v1/cells.controller.tspackages/backend/src/api/v1/documents.controller.tspackages/backend/src/api/v1/images.controller.tspackages/backend/src/api/v1/tabs.controller.tspackages/backend/src/document/document.service.tspackages/backend/src/image/image.constants.tspackages/backend/src/image/image.controller.tspackages/docs/src/view/pagination.tspackages/docs/src/view/peer-cursor.tspackages/docs/src/view/selection.tspackages/frontend/src/app/datasources/datasource-edit-dialog.tsxpackages/frontend/src/app/docs/docs-formatting-toolbar.tsxpackages/frontend/src/app/spreadsheet/charts/area-chart-renderer.tsxpackages/frontend/src/app/spreadsheet/charts/bar-chart-renderer.tsxpackages/frontend/src/app/spreadsheet/charts/chart-registry.tspackages/frontend/src/app/spreadsheet/charts/line-chart-renderer.tsxpackages/frontend/src/app/spreadsheet/charts/pie-chart-renderer.tsxpackages/frontend/src/app/spreadsheet/charts/scatter-chart-renderer.tsxpackages/frontend/src/components/color-picker-grid.tsxpackages/frontend/src/components/datasource-dialog.tsxpackages/frontend/src/components/datasource-form-fields.tsxpackages/frontend/src/components/formatting-toolbar.tsxpackages/sheets/src/model/worksheet/conditional-format.tspackages/sheets/src/model/worksheet/range-styles.ts
- Add type="button" and aria-label to ColorPickerGrid buttons - Return early in buildCellRangeRects when nested page line is not found, preventing baseY from staying 0 - Add autoComplete="new-password" to datasource password input - Reuse LegendPosition type in all chart renderers instead of duplicating the string union Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
ColorPickerGridcomponent — identical color grid + reset button pattern was duplicated 6 times acrossformatting-toolbar.tsxanddocs-formatting-toolbar.tsxDataSourceFormFieldscomponent — create and edit datasource dialogs had identical 7-input form layoutsAlignmentDropdownin docs toolbar — same 4-item alignment menu (Left/Center/Right/Justify with shortcuts) was duplicated in header/footer and body sectionsgetLegendPropshelper — same legend position calculation was duplicated across all 5 chart renderersNet result: -103 lines (252 added, 355 removed) across 12 files.
Test plan
pnpm verify:fastpasses (lint + all unit tests)pnpm verify:selfpasses (includes builds)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Refactor